Fix actor fields and transactions for API operations as they are recorded to DB.

This commit is contained in:
2026-01-23 20:19:33 +00:00
parent c2933d60c2
commit ae4c982a30
7 changed files with 91 additions and 70 deletions
-12
View File
@@ -136,7 +136,6 @@ async def admin_create_org(
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
)
from ..db import Org as OrgDC # local import to avoid cycles
from ..db import Role as RoleDC # local import to avoid cycles
actor = str(ctx.user.uuid)
org_uuid = uuid4()
@@ -145,17 +144,6 @@ async def admin_create_org(
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
db.create_organization(org, actor=actor)
# Automatically create Administration role with org admin permission
# The auth:org:admin permission is automatically created/enabled by create_organization
role_uuid = uuid4()
admin_role = RoleDC(
uuid=role_uuid,
org_uuid=org_uuid,
display_name="Administration",
permissions=["auth:org:admin"],
)
db.create_role(admin_role, actor=actor)
return {"uuid": str(org_uuid)}
+2 -2
View File
@@ -289,11 +289,11 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
if not auth:
return {"message": "Already logged out"}
try:
await get_session(auth, host=request.headers.get("host"))
s = await get_session(auth, host=request.headers.get("host"))
except ValueError:
return {"message": "Already logged out"}
with suppress(Exception):
db.delete_session(session_key(auth))
db.delete_session(session_key(auth), actor=str(s.user_uuid))
session.clear_session_cookie(response)
return {"message": "Logged out successfully"}
+20 -13
View File
@@ -16,7 +16,6 @@ import base64url
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from paskia import db, remoteauth
from paskia.authsession import create_session
from paskia.fastapi.session import infodict
from paskia.fastapi.wsutil import validate_origin, websocket_error_handler
from paskia.globals import passkey
@@ -334,9 +333,6 @@ async def websocket_remote_auth_permit(ws: WebSocket):
credential, webauthn_challenge, stored_cred, origin
)
# Update credential last_used
db.login(stored_cred.user_uuid, stored_cred)
# Create a session for the REQUESTING device
assert stored_cred.uuid is not None
@@ -346,7 +342,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
if request.action == "register":
# For registration, create a reset token for device addition
from paskia.authsession import expires
from paskia.util import tokens
from paskia.util import hostutil, tokens
token_str = passphrase.generate()
expiry = expires()
@@ -355,25 +351,36 @@ async def websocket_remote_auth_permit(ws: WebSocket):
key=tokens.reset_key(token_str),
expiry=expiry,
token_type="device addition",
actor=str(stored_cred.user_uuid),
)
reset_token = token_str
# Also create a session so the device is logged in?
# User requested: "We can make the flow always create a new session, but make additional tokens for other possibilities."
session_token = await create_session(
# Also create a session so the device is logged in
session_token = passphrase.generate()
normalized_host = hostutil.normalize_host(request.host)
db.login(
user_uuid=stored_cred.user_uuid,
credential_uuid=stored_cred.uuid,
host=request.host,
credential=stored_cred,
session_key=tokens.session_key(session_token),
host=normalized_host,
ip=request.ip,
user_agent=request.user_agent,
expiry=expires(),
)
else:
# Default login action
session_token = await create_session(
from paskia.authsession import expires
from paskia.util import hostutil, tokens
session_token = passphrase.generate()
normalized_host = hostutil.normalize_host(request.host)
db.login(
user_uuid=stored_cred.user_uuid,
credential_uuid=stored_cred.uuid,
host=request.host,
credential=stored_cred,
session_key=tokens.session_key(session_token),
host=normalized_host,
ip=request.ip,
user_agent=request.user_agent,
expiry=expires(),
)
# Complete the remote auth request (notifies the waiting device)
+4 -3
View File
@@ -55,7 +55,7 @@ async def user_update_display_name(
raise HTTPException(status_code=400, detail="display_name required")
if len(new_name) > 64:
raise HTTPException(status_code=400, detail="display_name too long")
db.update_user_display_name(s.user_uuid, new_name)
db.update_user_display_name(s.user_uuid, new_name, actor=str(s.user_uuid))
return {"status": "ok"}
@@ -69,7 +69,7 @@ async def api_logout_all(request: Request, response: Response, auth=AUTH_COOKIE)
raise authz.AuthException(
status_code=401, detail="Session expired", mode="login"
)
db.delete_sessions_for_user(s.user_uuid)
db.delete_sessions_for_user(s.user_uuid, actor=str(s.user_uuid))
session.clear_session_cookie(response)
return {"message": "Logged out from all hosts"}
@@ -103,7 +103,7 @@ async def api_delete_session(
if not target_session or target_session.user_uuid != current_session.user_uuid:
raise HTTPException(status_code=404, detail="Session not found")
db.delete_session(target_key)
db.delete_session(target_key, actor=str(current_session.user_uuid))
current_terminated = target_key == session_key(auth)
if current_terminated:
session.clear_session_cookie(response) # explicit because 200
@@ -149,6 +149,7 @@ async def api_create_link(
key=tokens.reset_key(token),
expiry=expiry,
token_type="device addition",
actor=str(s.user_uuid),
)
url = hostutil.reset_link_url(token)
return {
+18 -8
View File
@@ -3,12 +3,12 @@ from uuid import UUID
from fastapi import FastAPI, WebSocket
from paskia import db
from paskia.authsession import create_session, get_reset, get_session
from paskia.authsession import expires, get_reset, get_session
from paskia.fastapi import authz, remote
from paskia.fastapi.session import AUTH_COOKIE, infodict
from paskia.fastapi.wsutil import validate_origin, websocket_error_handler
from paskia.globals import passkey
from paskia.util import passphrase
from paskia.util import hostutil, passphrase
from paskia.util.tokens import create_token, session_key
# Create a FastAPI subapp for WebSocket endpoints
@@ -89,6 +89,7 @@ async def websocket_register_add(
host=host,
ip=metadata.get("ip"),
user_agent=metadata.get("user_agent"),
actor=str(user_uuid),
)
auth = token
@@ -140,18 +141,27 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
# Verify the credential matches the stored data
passkey.instance.auth_verify(credential, challenge, stored_cred, origin)
# Update both credential and user's last_seen timestamp
db.login(stored_cred.user_uuid, stored_cred)
# Create a session token for the authenticated user
# Create session and update user/credential in a single transaction
assert stored_cred.uuid is not None
metadata = infodict(ws, "auth")
token = await create_session(
token = create_token()
normalized_host = hostutil.normalize_host(host)
if not normalized_host:
raise ValueError("Host required for session creation")
hostname = normalized_host.split(":")[0]
rp_id = passkey.instance.rp_id
if not (hostname == rp_id or hostname.endswith(f".{rp_id}")):
raise ValueError(f"Host must be the same as or a subdomain of {rp_id}")
db.login(
user_uuid=stored_cred.user_uuid,
credential_uuid=stored_cred.uuid,
host=host,
credential=stored_cred,
session_key=session_key(token),
host=normalized_host,
ip=metadata.get("ip") or "",
user_agent=metadata.get("user_agent") or "",
expiry=expires(),
)
await ws.send_json(