diff --git a/api/users/users.py b/api/users/users.py index 9df6a1d8..c8a5d37e 100644 --- a/api/users/users.py +++ b/api/users/users.py @@ -1,6 +1,6 @@ from typing import Any -from fastapi import Path +from fastapi import Header, Path from nxtools import logging from ayon_server.api.clientinfo import ClientInfo @@ -8,7 +8,9 @@ from ayon_server.api.responses import EmptyResponse from ayon_server.auth.session import Session from ayon_server.auth.utils import validate_password +from ayon_server.config import ayonconfig from ayon_server.entities import UserEntity +from ayon_server.events import EventStream from ayon_server.exceptions import ( BadRequestException, ConflictException, @@ -118,6 +120,7 @@ async def create_user( put_data: NewUserModel, user: CurrentUser, user_name: UserName, + x_sender: str | None = Header(default=None), ) -> EmptyResponse: """Create a new user.""" @@ -147,19 +150,48 @@ async def create_user( raise BadRequestException("Only service users can have API keys") nuser.set_api_key(put_data.api_key) + event: dict[str, Any] = { + "topic": "entity.user.created", + "description": f"User {user.name} created", + "summary": {"entityName": user.name}, + } + await nuser.save() + await EventStream.dispatch( + sender=x_sender, + user=user.name, + **event, + ) return EmptyResponse() @router.delete("/{user_name}") -async def delete_user(user: CurrentUser, user_name: UserName) -> EmptyResponse: - logging.info(f"[DELETE] /users/{user_name}") +async def delete_user( + user: CurrentUser, + user_name: UserName, + x_sender: str | None = Header(default=None), +) -> EmptyResponse: if not user.is_manager: raise ForbiddenException target_user = await UserEntity.load(user_name) - await target_user.delete() + event: dict[str, Any] = { + "description": f"User {user_name} deleted", + "summary": {"entityName": user_name}, + } + if ayonconfig.audit_trail: + event["payload"] = { + "entityData": target_user.dict_simple(), + } + + await target_user.delete() + await EventStream.dispatch( + "entity.user.deleted", + sender=x_sender, + user=user.name, + **event, + ) return EmptyResponse() @@ -334,10 +366,18 @@ async def change_user_name( patch_data: ChangeUserNameRequestModel, user: CurrentUser, user_name: UserName, + x_sender: str | None = Header(default=None), ) -> EmptyResponse: if not user.is_manager: raise ForbiddenException + event: dict[str, Any] = { + "topic": "entity.user.renamed", + "description": f"Renamed user {user_name} to {patch_data.new_name}", + "summary": {"entityName": user_name}, + "payload": {"oldValue": user_name, "newValue": patch_data.new_name}, + } + async with Postgres.acquire() as conn: async with conn.transaction(): await conn.execute( @@ -371,6 +411,13 @@ async def change_user_name( async for session in Session.list(user_name): token = session.token await Session.delete(token) + + await EventStream.dispatch( + sender=x_sender, + user=user.name, + **event, + ) + return EmptyResponse() diff --git a/ayon_server/auth/session.py b/ayon_server/auth/session.py index ede24bc8..7ce4cffa 100644 --- a/ayon_server/auth/session.py +++ b/ayon_server/auth/session.py @@ -1,10 +1,9 @@ __all__ = ["Session"] import time -from typing import AsyncGenerator +from typing import Any, AsyncGenerator from fastapi import Request -from nxtools import logging from ayon_server.api.clientinfo import ClientInfo, get_client_info, get_real_ip from ayon_server.config import ayonconfig @@ -104,6 +103,8 @@ async def create( user: UserEntity, request: Request | None = None, token: str | None = None, + message: str = "User logged in", + event_payload: dict[str, Any] | None = None, ) -> SessionModel: """Create a new session for a given user.""" is_service = bool(token) @@ -118,13 +119,15 @@ async def create( is_service=is_service, client_info=client_info, ) + event_summary = client_info.dict() if client_info else {} await Redis.set(cls.ns, token, session.json()) if not user.is_service: await EventStream.dispatch( - "user.log_in", - description="User logged in", + "auth.login", + description=message, user=user.name, - summary=client_info.dict() if client_info else None, + summary=event_summary, + payload=event_payload, ) return session @@ -155,7 +158,7 @@ async def delete(cls, token: str, message: str = "User logged out") -> None: session = SessionModel(**json_loads(data)) if not session.user.data.get("isService"): await EventStream.dispatch( - "user.log_out", + "auth.logout", description=message, user=session.user.name, ) @@ -171,14 +174,13 @@ async def list( from the database. """ - async for _session_id, data in Redis.iterate("session"): + async for _, data in Redis.iterate("session"): + if data is None: + continue # this should never happen, but keeps mypy happy + session = SessionModel(**json_loads(data)) if cls.is_expired(session): - logging.info( - f"Removing expired session for user " - f"{session.user.name} {session.token}" - ) - await cls.delete(session.token) + await cls.delete(session.token, message="Session expired") continue if user_name is None or session.user.name == user_name: