From f49d1078ffc4cd2d4af4e8fb92d8ff920c34c841 Mon Sep 17 00:00:00 2001 From: edraft Date: Mon, 29 Sep 2025 08:31:59 +0200 Subject: [PATCH] Renamed AuthUsers -> Users & completed user gql #181 --- example/api/src/main.py | 16 +-- example/api/src/queries/hello.py | 18 +-- src/cpl-api/cpl/api/application/web_app.py | 5 +- .../cpl/api/middleware/authentication.py | 8 +- .../cpl/api/middleware/authorization.py | 4 +- src/cpl-api/cpl/api/middleware/request.py | 8 +- src/cpl-api/cpl/api/typing.py | 4 +- src/cpl-auth/cpl/auth/auth_module.py | 4 +- .../cpl/auth/permission/role_seeder.py | 4 +- src/cpl-auth/cpl/auth/schema/__init__.py | 4 +- .../_administration/{auth_user.py => user.py} | 20 ++-- .../{auth_user_dao.py => user_dao.py} | 16 +-- .../cpl/auth/schema/_permission/role_user.py | 6 +- .../cpl/auth/scripts/mysql/1-users.sql | 18 +-- .../cpl/auth/scripts/mysql/2-api-key.sql | 2 +- .../scripts/mysql/3-roles-permissions.sql | 28 ++--- .../scripts/mysql/4-api-key-permissions.sql | 2 +- .../cpl/auth/scripts/postgres/1-users.sql | 10 +- .../cpl/auth/scripts/postgres/2-api-key.sql | 2 +- .../scripts/postgres/3-roles-permissions.sql | 18 +-- .../postgres/4-api-key-permissions.sql | 2 +- src/cpl-core/cpl/core/ctx/user_context.py | 8 +- .../cpl/database/abc/db_model_abc.py | 6 +- .../cpl/database/abc/db_model_dao_abc.py | 2 +- .../cpl/database/table_manager.py | 10 +- .../cpl/graphql/application/graphql_app.py | 13 ++ .../administration/auth_user_graph_type.py | 12 -- .../auth/administration/user/__init__.py | 0 .../user_filter.py} | 4 +- .../administration/user/user_graph_type.py | 12 ++ .../auth/administration/user/user_input.py | 23 ++++ .../auth/administration/user/user_mutation.py | 112 ++++++++++++++++++ .../cpl/graphql/auth/graphql_auth_module.py | 22 +++- src/cpl-graphql/cpl/graphql/graphql_module.py | 6 - src/cpl-graphql/cpl/graphql/query_context.py | 4 +- .../cpl/graphql/schema/db_model_graph_type.py | 7 +- .../graphql/schema/filter/db_model_filter.py | 6 +- .../cpl/graphql/schema/filter/filter.py | 10 +- .../cpl/graphql/schema/mutation.py | 76 +++++++++++- src/cpl-graphql/cpl/graphql/service/schema.py | 5 +- 40 files changed, 387 insertions(+), 150 deletions(-) rename src/cpl-auth/cpl/auth/schema/_administration/{auth_user.py => user.py} (78%) rename src/cpl-auth/cpl/auth/schema/_administration/{auth_user_dao.py => user_dao.py} (83%) delete mode 100644 src/cpl-graphql/cpl/graphql/auth/administration/auth_user_graph_type.py create mode 100644 src/cpl-graphql/cpl/graphql/auth/administration/user/__init__.py rename src/cpl-graphql/cpl/graphql/auth/administration/{auth_user_filter.py => user/user_filter.py} (80%) create mode 100644 src/cpl-graphql/cpl/graphql/auth/administration/user/user_graph_type.py create mode 100644 src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py create mode 100644 src/cpl-graphql/cpl/graphql/auth/administration/user/user_mutation.py diff --git a/example/api/src/main.py b/example/api/src/main.py index c57d0e39..d4fae505 100644 --- a/example/api/src/main.py +++ b/example/api/src/main.py @@ -1,17 +1,18 @@ from starlette.responses import JSONResponse from api.src.queries.cities import CityGraphType, CityFilter, CitySort -from api.src.queries.hello import UserGraphType#, AuthUserFilter, AuthUserSort, AuthUserGraphType +from api.src.queries.hello import UserGraphType#, UserFilter, UserSort, UserGraphType from api.src.queries.user import UserFilter, UserSort from cpl.api.api_module import ApiModule from cpl.application.application_builder import ApplicationBuilder -from cpl.auth.schema import AuthUser, Role +from cpl.auth.schema import User, Role from cpl.core.configuration import Configuration from cpl.core.console import Console from cpl.core.environment import Environment from cpl.core.utils.cache import Cache from cpl.database.mysql.mysql_module import MySQLModule from cpl.graphql.application.graphql_app import GraphQLApp +from cpl.graphql.auth.graphql_auth_module import GraphQLAuthModule from cpl.graphql.graphql_module import GraphQLModule from model.author_dao import AuthorDao from model.author_query import AuthorGraphType, AuthorFilter, AuthorSort @@ -38,8 +39,9 @@ def main(): .add_module(MySQLModule) .add_module(ApiModule) .add_module(GraphQLModule) + .add_module(GraphQLAuthModule) .add_scoped(ScopedService) - .add_cache(AuthUser) + .add_cache(User) .add_cache(Role) .add_transient(CityGraphType) .add_transient(CityFilter) @@ -47,9 +49,9 @@ def main(): .add_transient(UserGraphType) .add_transient(UserFilter) .add_transient(UserSort) - # .add_transient(AuthUserGraphType) - # .add_transient(AuthUserFilter) - # .add_transient(AuthUserSort) + # .add_transient(UserGraphType) + # .add_transient(UserFilter) + # .add_transient(UserSort) .add_transient(HelloQuery) # test data .add_singleton(TestDataSeeder) @@ -100,7 +102,7 @@ def main(): app.with_permissions(PostPermissions) provider = builder.service_provider - user_cache = provider.get_service(Cache[AuthUser]) + user_cache = provider.get_service(Cache[User]) role_cache = provider.get_service(Cache[Role]) if role_cache == user_cache: diff --git a/example/api/src/queries/hello.py b/example/api/src/queries/hello.py index 88d9af27..c53ce008 100644 --- a/example/api/src/queries/hello.py +++ b/example/api/src/queries/hello.py @@ -1,7 +1,7 @@ from api.src.queries.cities import CityFilter, CitySort, CityGraphType, City from api.src.queries.user import User, UserFilter, UserSort, UserGraphType from cpl.api.middleware.request import get_request -from cpl.auth.schema import AuthUserDao, AuthUser +from cpl.auth.schema import UserDao, User from cpl.graphql.schema.filter.filter import Filter from cpl.graphql.schema.graph_type import GraphType from cpl.graphql.schema.query import Query @@ -11,20 +11,20 @@ from cpl.graphql.schema.sort.sort_order import SortOrder users = [User(i, f"User {i}") for i in range(1, 101)] cities = [City(i, f"City {i}") for i in range(1, 101)] -# class AuthUserFilter(Filter[AuthUser]): +# class UserFilter(Filter[User]): # def __init__(self): # Filter.__init__(self) # self.field("id", int) # self.field("username", str) # # -# class AuthUserSort(Sort[AuthUser]): +# class UserSort(Sort[User]): # def __init__(self): # Sort.__init__(self) # self.field("id", SortOrder) # self.field("username", SortOrder) # -# class AuthUserGraphType(GraphType[AuthUser]): +# class UserGraphType(GraphType[User]): # # def __init__(self): # GraphType.__init__(self) @@ -61,9 +61,9 @@ class HelloQuery(Query): resolver=lambda: cities, ) # self.dao_collection_field( - # AuthUserGraphType, - # AuthUserDao, - # "authUsers", - # AuthUserFilter, - # AuthUserSort, + # UserGraphType, + # UserDao, + # "Users", + # UserFilter, + # UserSort, # ) diff --git a/src/cpl-api/cpl/api/application/web_app.py b/src/cpl-api/cpl/api/application/web_app.py index b63b4700..f994444e 100644 --- a/src/cpl-api/cpl/api/application/web_app.py +++ b/src/cpl-api/cpl/api/application/web_app.py @@ -214,6 +214,9 @@ class WebApp(WebAppABC): self.with_middleware(AuthorizationMiddleware) return self + async def _log_before_startup(self): + self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}") + async def main(self): self._logger.debug(f"Preparing API") self._validate_policies() @@ -237,7 +240,7 @@ class WebApp(WebAppABC): else: app = self._app - self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}") + await self._log_before_startup() config = uvicorn.Config( app, host=self._api_settings.host, port=self._api_settings.port, log_config=None, loop="asyncio" diff --git a/src/cpl-api/cpl/api/middleware/authentication.py b/src/cpl-api/cpl/api/middleware/authentication.py index 9b45c076..8b40cdd1 100644 --- a/src/cpl-api/cpl/api/middleware/authentication.py +++ b/src/cpl-api/cpl/api/middleware/authentication.py @@ -7,13 +7,13 @@ from cpl.api.logger import APILogger from cpl.api.middleware.request import get_request from cpl.api.router import Router from cpl.auth.keycloak import KeycloakClient -from cpl.auth.schema import AuthUserDao, AuthUser +from cpl.auth.schema import UserDao, User from cpl.core.ctx import set_user class AuthenticationMiddleware(ASGIMiddleware): - def __init__(self, app, logger: APILogger, keycloak: KeycloakClient, user_dao: AuthUserDao): + def __init__(self, app, logger: APILogger, keycloak: KeycloakClient, user_dao: UserDao): ASGIMiddleware.__init__(self, app) self._logger = logger @@ -72,12 +72,12 @@ class AuthenticationMiddleware(ASGIMiddleware): return await self._call_next(scope, receive, send) - async def _get_or_crate_user(self, keycloak_id: str) -> AuthUser: + async def _get_or_crate_user(self, keycloak_id: str) -> User: existing = await self._user_dao.find_by_keycloak_id(keycloak_id) if existing is not None: return existing - user = AuthUser(0, keycloak_id) + user = User(0, keycloak_id) uid = await self._user_dao.create(user) return await self._user_dao.get_by_id(uid) diff --git a/src/cpl-api/cpl/api/middleware/authorization.py b/src/cpl-api/cpl/api/middleware/authorization.py index b0b0d18c..64347cdc 100644 --- a/src/cpl-api/cpl/api/middleware/authorization.py +++ b/src/cpl-api/cpl/api/middleware/authorization.py @@ -7,13 +7,13 @@ from cpl.api.middleware.request import get_request from cpl.api.model.validation_match import ValidationMatch from cpl.api.registry.policy import PolicyRegistry from cpl.api.router import Router -from cpl.auth.schema._administration.auth_user_dao import AuthUserDao +from cpl.auth.schema._administration.user_dao import UserDao from cpl.core.ctx.user_context import get_user class AuthorizationMiddleware(ASGIMiddleware): - def __init__(self, app, logger: APILogger, policies: PolicyRegistry, user_dao: AuthUserDao): + def __init__(self, app, logger: APILogger, policies: PolicyRegistry, user_dao: UserDao): ASGIMiddleware.__init__(self, app) self._logger = logger diff --git a/src/cpl-api/cpl/api/middleware/request.py b/src/cpl-api/cpl/api/middleware/request.py index 6ddea35c..05a291e3 100644 --- a/src/cpl-api/cpl/api/middleware/request.py +++ b/src/cpl-api/cpl/api/middleware/request.py @@ -10,8 +10,8 @@ from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware from cpl.api.logger import APILogger from cpl.api.typing import TRequest from cpl.auth.keycloak.keycloak_client import KeycloakClient -from cpl.auth.schema import AuthUser -from cpl.auth.schema._administration.auth_user_dao import AuthUserDao +from cpl.auth.schema import User +from cpl.auth.schema._administration.user_dao import UserDao from cpl.core.ctx import set_user from cpl.dependency.inject import inject from cpl.dependency.service_provider import ServiceProvider @@ -22,7 +22,7 @@ _request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", defa class RequestMiddleware(ASGIMiddleware): def __init__( - self, app, provider: ServiceProvider, logger: APILogger, keycloak: KeycloakClient, user_dao: AuthUserDao + self, app, provider: ServiceProvider, logger: APILogger, keycloak: KeycloakClient, user_dao: UserDao ): ASGIMiddleware.__init__(self, app) @@ -80,7 +80,7 @@ class RequestMiddleware(ASGIMiddleware): user = await self._user_dao.find_by_keycloak_id(keycloak_id) if not user: - user = AuthUser(0, keycloak_id) + user = User(0, keycloak_id) uid = await self._user_dao.create(user) user = await self._user_dao.get_by_id(uid) diff --git a/src/cpl-api/cpl/api/typing.py b/src/cpl-api/cpl/api/typing.py index a62d4927..8d5f0c73 100644 --- a/src/cpl-api/cpl/api/typing.py +++ b/src/cpl-api/cpl/api/typing.py @@ -7,7 +7,7 @@ from starlette.types import ASGIApp from starlette.websockets import WebSocket from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware -from cpl.auth.schema import AuthUser +from cpl.auth.schema import User TRequest = Union[Request, WebSocket] TEndpoint = Callable[[TRequest, ...], Awaitable[Response]] | Callable[[TRequest, ...], Response] @@ -18,5 +18,5 @@ PartialMiddleware = Union[ Middleware, Callable[[ASGIApp], ASGIApp], ] -PolicyResolver = Callable[[AuthUser], bool | Awaitable[bool]] +PolicyResolver = Callable[[User], bool | Awaitable[bool]] PolicyInput = Union[dict[str, PolicyResolver], "Policy"] diff --git a/src/cpl-auth/cpl/auth/auth_module.py b/src/cpl-auth/cpl/auth/auth_module.py index ea2b8582..aa1f7bef 100644 --- a/src/cpl-auth/cpl/auth/auth_module.py +++ b/src/cpl-auth/cpl/auth/auth_module.py @@ -12,7 +12,7 @@ from cpl.dependency.service_provider import ServiceProvider from .keycloak.keycloak_admin import KeycloakAdmin from .keycloak.keycloak_client import KeycloakClient from .schema._administration.api_key_dao import ApiKeyDao -from .schema._administration.auth_user_dao import AuthUserDao +from .schema._administration.user_dao import UserDao from .schema._permission.api_key_permission_dao import ApiKeyPermissionDao from .schema._permission.permission_dao import PermissionDao from .schema._permission.role_dao import RoleDao @@ -26,7 +26,7 @@ class AuthModule(Module): singleton = [ KeycloakClient, KeycloakAdmin, - AuthUserDao, + UserDao, ApiKeyDao, ApiKeyPermissionDao, PermissionDao, diff --git a/src/cpl-auth/cpl/auth/permission/role_seeder.py b/src/cpl-auth/cpl/auth/permission/role_seeder.py index 15925299..b6a2db43 100644 --- a/src/cpl-auth/cpl/auth/permission/role_seeder.py +++ b/src/cpl-auth/cpl/auth/permission/role_seeder.py @@ -6,7 +6,7 @@ from cpl.auth.schema import ( RolePermissionDao, ApiKeyDao, ApiKeyPermissionDao, - AuthUserDao, + UserDao, RoleUserDao, RoleUser, ) @@ -23,7 +23,7 @@ class RoleSeeder(DataSeederABC): role_permission_dao: RolePermissionDao, api_key_dao: ApiKeyDao, api_key_permission_dao: ApiKeyPermissionDao, - user_dao: AuthUserDao, + user_dao: UserDao, role_user_dao: RoleUserDao, ): DataSeederABC.__init__(self) diff --git a/src/cpl-auth/cpl/auth/schema/__init__.py b/src/cpl-auth/cpl/auth/schema/__init__.py index cdb4b9d1..af3373ee 100644 --- a/src/cpl-auth/cpl/auth/schema/__init__.py +++ b/src/cpl-auth/cpl/auth/schema/__init__.py @@ -1,7 +1,7 @@ from ._administration.api_key import ApiKey from ._administration.api_key_dao import ApiKeyDao -from ._administration.auth_user import AuthUser -from ._administration.auth_user_dao import AuthUserDao +from ._administration.user import User +from ._administration.user_dao import UserDao from ._permission.api_key_permission import ApiKeyPermission from ._permission.api_key_permission_dao import ApiKeyPermissionDao diff --git a/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py b/src/cpl-auth/cpl/auth/schema/_administration/user.py similarity index 78% rename from src/cpl-auth/cpl/auth/schema/_administration/auth_user.py rename to src/cpl-auth/cpl/auth/schema/_administration/user.py index 950a321c..f20740e6 100644 --- a/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py +++ b/src/cpl-auth/cpl/auth/schema/_administration/user.py @@ -13,7 +13,7 @@ from cpl.database.logger import DBLogger from cpl.dependency import get_provider -class AuthUser(DbModelABC[Self]): +class User(DbModelABC[Self]): def __init__( self, id: SerialId, @@ -69,21 +69,21 @@ class AuthUser(DbModelABC[Self]): @async_property async def permissions(self): - from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + from cpl.auth.schema._administration.user_dao import UserDao - auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) - return await auth_user_dao.get_permissions(self.id) + user_dao: UserDao = get_provider().get_service(UserDao) + return await user_dao.get_permissions(self.id) async def has_permission(self, permission: Permissions) -> bool: - from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + from cpl.auth.schema._administration.user_dao import UserDao - auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) - return await auth_user_dao.has_permission(self.id, permission) + user_dao: UserDao = get_provider().get_service(UserDao) + return await user_dao.has_permission(self.id, permission) async def anonymize(self): - from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + from cpl.auth.schema._administration.user_dao import UserDao - auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) + user_dao: UserDao = get_provider().get_service(UserDao) self._keycloak_id = str(uuid.UUID(int=0)) - await auth_user_dao.update(self) + await user_dao.update(self) diff --git a/src/cpl-auth/cpl/auth/schema/_administration/auth_user_dao.py b/src/cpl-auth/cpl/auth/schema/_administration/user_dao.py similarity index 83% rename from src/cpl-auth/cpl/auth/schema/_administration/auth_user_dao.py rename to src/cpl-auth/cpl/auth/schema/_administration/user_dao.py index bf59a534..206ab553 100644 --- a/src/cpl-auth/cpl/auth/schema/_administration/auth_user_dao.py +++ b/src/cpl-auth/cpl/auth/schema/_administration/user_dao.py @@ -3,21 +3,21 @@ from typing import Optional, Union from cpl.auth.permission.permissions import Permissions from cpl.auth.schema._permission.permission_dao import PermissionDao from cpl.auth.schema._permission.permission import Permission -from cpl.auth.schema._administration.auth_user import AuthUser +from cpl.auth.schema._administration.user import User from cpl.database import TableManager from cpl.database.abc import DbModelDaoABC from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder from cpl.dependency.context import get_provider -class AuthUserDao(DbModelDaoABC[AuthUser]): +class UserDao(DbModelDaoABC[User]): def __init__(self, permission_dao: PermissionDao): - DbModelDaoABC.__init__(self, AuthUser, TableManager.get("auth_users")) + DbModelDaoABC.__init__(self, User, TableManager.get("users")) self._permissions = permission_dao - self.attribute(AuthUser.keycloak_id, str) + self.attribute(User.keycloak_id, str) async def get_users(): return [(x.id, x.username, x.email) for x in await self.get_all()] @@ -31,11 +31,11 @@ class AuthUserDao(DbModelDaoABC[AuthUser]): .with_value_getter(get_users) ) - async def get_by_keycloak_id(self, keycloak_id: str) -> AuthUser: - return await self.get_single_by({AuthUser.keycloak_id: keycloak_id}) + async def get_by_keycloak_id(self, keycloak_id: str) -> User: + return await self.get_single_by({User.keycloak_id: keycloak_id}) - async def find_by_keycloak_id(self, keycloak_id: str) -> Optional[AuthUser]: - return await self.find_single_by({AuthUser.keycloak_id: keycloak_id}) + async def find_by_keycloak_id(self, keycloak_id: str) -> Optional[User]: + return await self.find_single_by({User.keycloak_id: keycloak_id}) async def has_permission(self, user_id: int, permission: Union[Permissions, str]) -> bool: from cpl.auth.schema._permission.permission_dao import PermissionDao diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role_user.py b/src/cpl-auth/cpl/auth/schema/_permission/role_user.py index 90c4e05c..53806c9c 100644 --- a/src/cpl-auth/cpl/auth/schema/_permission/role_user.py +++ b/src/cpl-auth/cpl/auth/schema/_permission/role_user.py @@ -29,10 +29,10 @@ class RoleUser(DbJoinModelABC): @async_property async def user(self): - from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + from cpl.auth.schema._administration.user_dao import UserDao - auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) - return await auth_user_dao.get_by_id(self._user_id) + user_dao: UserDao = get_provider().get_service(UserDao) + return await user_dao.get_by_id(self._user_id) @property def role_id(self) -> int: diff --git a/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql b/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql index c3e09082..2226a9c2 100644 --- a/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql +++ b/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS administration_auth_users +CREATE TABLE IF NOT EXISTS administration_users ( id INT AUTO_INCREMENT PRIMARY KEY, keycloakId CHAR(36) NOT NULL, @@ -9,10 +9,10 @@ CREATE TABLE IF NOT EXISTS administration_auth_users updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT UC_KeycloakId UNIQUE (keycloakId), - CONSTRAINT FK_EditorId FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_EditorId FOREIGN KEY (editorId) REFERENCES administration_users (id) ); -CREATE TABLE IF NOT EXISTS administration_auth_users_history +CREATE TABLE IF NOT EXISTS administration_users_history ( id INT NOT NULL, keycloakId CHAR(36) NOT NULL, @@ -23,22 +23,22 @@ CREATE TABLE IF NOT EXISTS administration_auth_users_history updated TIMESTAMP NOT NULL ); -CREATE TRIGGER TR_administration_auth_usersUpdate +CREATE TRIGGER TR_administration_usersUpdate AFTER UPDATE - ON administration_auth_users + ON administration_users FOR EACH ROW BEGIN - INSERT INTO administration_auth_users_history + INSERT INTO administration_users_history (id, keycloakId, deleted, editorId, created, updated) VALUES (OLD.id, OLD.keycloakId, OLD.deleted, OLD.editorId, OLD.created, NOW()); END; -CREATE TRIGGER TR_administration_auth_usersDelete +CREATE TRIGGER TR_administration_usersDelete AFTER DELETE - ON administration_auth_users + ON administration_users FOR EACH ROW BEGIN - INSERT INTO administration_auth_users_history + INSERT INTO administration_users_history (id, keycloakId, deleted, editorId, created, updated) VALUES (OLD.id, OLD.keycloakId, 1, OLD.editorId, OLD.created, NOW()); END; \ No newline at end of file diff --git a/src/cpl-auth/cpl/auth/scripts/mysql/2-api-key.sql b/src/cpl-auth/cpl/auth/scripts/mysql/2-api-key.sql index 134c6c78..09418f91 100644 --- a/src/cpl-auth/cpl/auth/scripts/mysql/2-api-key.sql +++ b/src/cpl-auth/cpl/auth/scripts/mysql/2-api-key.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS administration_api_keys CONSTRAINT UC_Identifier_Key UNIQUE (identifier, keyString), CONSTRAINT UC_Key UNIQUE (keyString), - CONSTRAINT FK_ApiKeys_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_ApiKeys_Editor FOREIGN KEY (editorId) REFERENCES administration_users (id) ); CREATE TABLE IF NOT EXISTS administration_api_keys_history diff --git a/src/cpl-auth/cpl/auth/scripts/mysql/3-roles-permissions.sql b/src/cpl-auth/cpl/auth/scripts/mysql/3-roles-permissions.sql index 63a58fbf..23b4ecc8 100644 --- a/src/cpl-auth/cpl/auth/scripts/mysql/3-roles-permissions.sql +++ b/src/cpl-auth/cpl/auth/scripts/mysql/3-roles-permissions.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS permission_permissions created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT UQ_PermissionName UNIQUE (name), - CONSTRAINT FK_Permissions_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_Permissions_Editor FOREIGN KEY (editorId) REFERENCES administration_users (id) ); CREATE TABLE IF NOT EXISTS permission_permissions_history @@ -52,7 +52,7 @@ CREATE TABLE IF NOT EXISTS permission_roles created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT UQ_RoleName UNIQUE (name), - CONSTRAINT FK_Roles_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_Roles_Editor FOREIGN KEY (editorId) REFERENCES administration_users (id) ); CREATE TABLE IF NOT EXISTS permission_roles_history @@ -98,7 +98,7 @@ CREATE TABLE IF NOT EXISTS permission_role_permissions CONSTRAINT UQ_RolePermission UNIQUE (roleId, permissionId), CONSTRAINT FK_RolePermissions_Role FOREIGN KEY (roleId) REFERENCES permission_roles (id) ON DELETE CASCADE, CONSTRAINT FK_RolePermissions_Permission FOREIGN KEY (permissionId) REFERENCES permission_permissions (id) ON DELETE CASCADE, - CONSTRAINT FK_RolePermissions_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_RolePermissions_Editor FOREIGN KEY (editorId) REFERENCES administration_users (id) ); CREATE TABLE IF NOT EXISTS permission_role_permissions_history @@ -132,7 +132,7 @@ BEGIN VALUES (OLD.id, OLD.roleId, OLD.permissionId, 1, OLD.editorId, OLD.created, NOW()); END; -CREATE TABLE IF NOT EXISTS permission_role_auth_users +CREATE TABLE IF NOT EXISTS permission_role_users ( id INT AUTO_INCREMENT PRIMARY KEY, roleId INT NOT NULL, @@ -142,12 +142,12 @@ CREATE TABLE IF NOT EXISTS permission_role_auth_users created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT UQ_RoleUser UNIQUE (roleId, userId), - CONSTRAINT FK_Roleauth_users_Role FOREIGN KEY (roleId) REFERENCES permission_roles (id) ON DELETE CASCADE, - CONSTRAINT FK_Roleauth_users_User FOREIGN KEY (userId) REFERENCES administration_auth_users (id) ON DELETE CASCADE, - CONSTRAINT FK_Roleauth_users_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_Roleusers_Role FOREIGN KEY (roleId) REFERENCES permission_roles (id) ON DELETE CASCADE, + CONSTRAINT FK_Roleusers_User FOREIGN KEY (userId) REFERENCES administration_users (id) ON DELETE CASCADE, + CONSTRAINT FK_Roleusers_Editor FOREIGN KEY (editorId) REFERENCES administration_users (id) ); -CREATE TABLE IF NOT EXISTS permission_role_auth_users_history +CREATE TABLE IF NOT EXISTS permission_role_users_history ( id INT NOT NULL, roleId INT NOT NULL, @@ -158,22 +158,22 @@ CREATE TABLE IF NOT EXISTS permission_role_auth_users_history updated TIMESTAMP NOT NULL ); -CREATE TRIGGER TR_Roleauth_usersUpdate +CREATE TRIGGER TR_RoleusersUpdate AFTER UPDATE - ON permission_role_auth_users + ON permission_role_users FOR EACH ROW BEGIN - INSERT INTO permission_role_auth_users_history + INSERT INTO permission_role_users_history (id, roleId, userId, deleted, editorId, created, updated) VALUES (OLD.id, OLD.roleId, OLD.userId, OLD.deleted, OLD.editorId, OLD.created, NOW()); END; -CREATE TRIGGER TR_Roleauth_usersDelete +CREATE TRIGGER TR_RoleusersDelete AFTER DELETE - ON permission_role_auth_users + ON permission_role_users FOR EACH ROW BEGIN - INSERT INTO permission_role_auth_users_history + INSERT INTO permission_role_users_history (id, roleId, userId, deleted, editorId, created, updated) VALUES (OLD.id, OLD.roleId, OLD.userId, 1, OLD.editorId, OLD.created, NOW()); END; diff --git a/src/cpl-auth/cpl/auth/scripts/mysql/4-api-key-permissions.sql b/src/cpl-auth/cpl/auth/scripts/mysql/4-api-key-permissions.sql index 8f8253fd..3effa6c0 100644 --- a/src/cpl-auth/cpl/auth/scripts/mysql/4-api-key-permissions.sql +++ b/src/cpl-auth/cpl/auth/scripts/mysql/4-api-key-permissions.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS permission_api_key_permissions CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId), CONSTRAINT FK_ApiKeyPermissions_ApiKey FOREIGN KEY (apiKeyId) REFERENCES administration_api_keys (id) ON DELETE CASCADE, CONSTRAINT FK_ApiKeyPermissions_Permission FOREIGN KEY (permissionId) REFERENCES permission_permissions (id) ON DELETE CASCADE, - CONSTRAINT FK_ApiKeyPermissions_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) + CONSTRAINT FK_ApiKeyPermissions_Editor FOREIGN KEY (editorId) REFERENCES administration_users (id) ); CREATE TABLE IF NOT EXISTS permission_api_key_permissions_history diff --git a/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql b/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql index 41d15483..1735852a 100644 --- a/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql +++ b/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql @@ -1,26 +1,26 @@ CREATE SCHEMA IF NOT EXISTS administration; -CREATE TABLE IF NOT EXISTS administration.auth_users +CREATE TABLE IF NOT EXISTS administration.users ( id SERIAL PRIMARY KEY, keycloakId UUID NOT NULL, -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT UC_KeycloakId UNIQUE (keycloakId) ); -CREATE TABLE IF NOT EXISTS administration.auth_users_history +CREATE TABLE IF NOT EXISTS administration.users_history ( - LIKE administration.auth_users + LIKE administration.users ); CREATE TRIGGER users_history_trigger BEFORE INSERT OR UPDATE OR DELETE - ON administration.auth_users + ON administration.users FOR EACH ROW EXECUTE FUNCTION public.history_trigger_function(); diff --git a/src/cpl-auth/cpl/auth/scripts/postgres/2-api-key.sql b/src/cpl-auth/cpl/auth/scripts/postgres/2-api-key.sql index 9944d667..e96ed708 100644 --- a/src/cpl-auth/cpl/auth/scripts/postgres/2-api-key.sql +++ b/src/cpl-auth/cpl/auth/scripts/postgres/2-api-key.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS administration.api_keys keyString VARCHAR(255) NOT NULL, -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), diff --git a/src/cpl-auth/cpl/auth/scripts/postgres/3-roles-permissions.sql b/src/cpl-auth/cpl/auth/scripts/postgres/3-roles-permissions.sql index 72400191..8ac5e1b1 100644 --- a/src/cpl-auth/cpl/auth/scripts/postgres/3-roles-permissions.sql +++ b/src/cpl-auth/cpl/auth/scripts/postgres/3-roles-permissions.sql @@ -9,7 +9,7 @@ CREATE TABLE permission.permissions -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT UQ_PermissionName UNIQUE (name) @@ -35,7 +35,7 @@ CREATE TABLE permission.roles -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT UQ_RoleName UNIQUE (name) @@ -61,7 +61,7 @@ CREATE TABLE permission.role_permissions -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT UQ_RolePermission UNIQUE (RoleId, permissionId) @@ -79,27 +79,27 @@ CREATE TRIGGER versioning_trigger EXECUTE PROCEDURE public.history_trigger_function(); -- Role user -CREATE TABLE permission.role_auth_users +CREATE TABLE permission.role_users ( id SERIAL PRIMARY KEY, RoleId INT NOT NULL REFERENCES permission.roles (id) ON DELETE CASCADE, - UserId INT NOT NULL REFERENCES administration.auth_users (id) ON DELETE CASCADE, + UserId INT NOT NULL REFERENCES administration.users (id) ON DELETE CASCADE, -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT UQ_RoleUser UNIQUE (RoleId, UserId) ); -CREATE TABLE permission.role_auth_users_history +CREATE TABLE permission.role_users_history ( - LIKE permission.role_auth_users + LIKE permission.role_users ); CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE - ON permission.role_auth_users + ON permission.role_users FOR EACH ROW EXECUTE PROCEDURE public.history_trigger_function(); \ No newline at end of file diff --git a/src/cpl-auth/cpl/auth/scripts/postgres/4-api-key-permissions.sql b/src/cpl-auth/cpl/auth/scripts/postgres/4-api-key-permissions.sql index 18e0d706..e0d677bb 100644 --- a/src/cpl-auth/cpl/auth/scripts/postgres/4-api-key-permissions.sql +++ b/src/cpl-auth/cpl/auth/scripts/postgres/4-api-key-permissions.sql @@ -6,7 +6,7 @@ CREATE TABLE permission.api_key_permissions -- for history deleted BOOLEAN NOT NULL DEFAULT FALSE, - editorId INT NULL REFERENCES administration.auth_users (id), + editorId INT NULL REFERENCES administration.users (id), created timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId) diff --git a/src/cpl-core/cpl/core/ctx/user_context.py b/src/cpl-core/cpl/core/ctx/user_context.py index a60d69f9..7aaa3584 100644 --- a/src/cpl-core/cpl/core/ctx/user_context.py +++ b/src/cpl-core/cpl/core/ctx/user_context.py @@ -1,13 +1,13 @@ from contextvars import ContextVar from typing import Optional -from cpl.auth.schema._administration.auth_user import AuthUser +from cpl.auth.schema._administration.user import User from cpl.dependency import get_provider -_user_context: ContextVar[Optional[AuthUser]] = ContextVar("user", default=None) +_user_context: ContextVar[Optional[User]] = ContextVar("user", default=None) -def set_user(user: Optional[AuthUser]): +def set_user(user: Optional[User]): from cpl.core.log.logger_abc import LoggerABC logger = get_provider().get_service(LoggerABC) @@ -15,5 +15,5 @@ def set_user(user: Optional[AuthUser]): _user_context.set(user) -def get_user() -> Optional[AuthUser]: +def get_user() -> Optional[User]: return _user_context.get() diff --git a/src/cpl-database/cpl/database/abc/db_model_abc.py b/src/cpl-database/cpl/database/abc/db_model_abc.py index 4f38a8de..3272bf67 100644 --- a/src/cpl-database/cpl/database/abc/db_model_abc.py +++ b/src/cpl-database/cpl/database/abc/db_model_abc.py @@ -49,11 +49,11 @@ class DbModelABC(ABC, Generic[T]): if self._editor_id is None: return None - from cpl.auth.schema import AuthUserDao + from cpl.auth.schema import UserDao - auth_user_dao = get_provider().get_service(AuthUserDao) + user_dao = get_provider().get_service(UserDao) - return await auth_user_dao.get_by_id(self._editor_id) + return await user_dao.get_by_id(self._editor_id) @property def created(self) -> datetime: diff --git a/src/cpl-database/cpl/database/abc/db_model_dao_abc.py b/src/cpl-database/cpl/database/abc/db_model_dao_abc.py index 9d9bfef6..873ba4fd 100644 --- a/src/cpl-database/cpl/database/abc/db_model_dao_abc.py +++ b/src/cpl-database/cpl/database/abc/db_model_dao_abc.py @@ -18,7 +18,7 @@ class DbModelDaoABC[T_DBM](DataAccessObjectABC[T_DBM]): self.attribute(DbModelABC.editor_id, int, db_name="editorId", ignore=True) # handled by db trigger self.reference( - "editor", "id", DbModelABC.editor_id, TableManager.get("auth_users") + "editor", "id", DbModelABC.editor_id, TableManager.get("users") ) # not relevant for updates due to editor_id self.attribute(DbModelABC.created, datetime, ignore=True) # handled by db trigger diff --git a/src/cpl-database/cpl/database/table_manager.py b/src/cpl-database/cpl/database/table_manager.py index 2d5ac533..7ca8d4e9 100644 --- a/src/cpl-database/cpl/database/table_manager.py +++ b/src/cpl-database/cpl/database/table_manager.py @@ -7,9 +7,9 @@ class TableManager: ServerTypes.POSTGRES: "system._executed_migrations", ServerTypes.MYSQL: "system__executed_migrations", }, - "auth_users": { - ServerTypes.POSTGRES: "administration.auth_users", - ServerTypes.MYSQL: "administration_auth_users", + "users": { + ServerTypes.POSTGRES: "administration.users", + ServerTypes.MYSQL: "administration_users", }, "api_keys": { ServerTypes.POSTGRES: "administration.api_keys", @@ -32,8 +32,8 @@ class TableManager: ServerTypes.MYSQL: "permission_role_permissions", }, "role_users": { - ServerTypes.POSTGRES: "permission.role_auth_users", - ServerTypes.MYSQL: "permission_role_auth_users", + ServerTypes.POSTGRES: "permission.role_users", + ServerTypes.MYSQL: "permission_role_users", }, } diff --git a/src/cpl-graphql/cpl/graphql/application/graphql_app.py b/src/cpl-graphql/cpl/graphql/application/graphql_app.py index ad4b06f0..bb422941 100644 --- a/src/cpl-graphql/cpl/graphql/application/graphql_app.py +++ b/src/cpl-graphql/cpl/graphql/application/graphql_app.py @@ -16,6 +16,9 @@ class GraphQLApp(WebApp): def __init__(self, services: ServiceProvider, modules: Modules): WebApp.__init__(self, services, modules, [GraphQLModule]) + self._with_graphiql = False + self._with_playground = False + def with_graphql( self, authentication: bool = False, @@ -57,6 +60,7 @@ class GraphQLApp(WebApp): policies=policies, match=match, ) + self._with_graphiql = True return self def with_playground( @@ -77,4 +81,13 @@ class GraphQLApp(WebApp): policies=policies, match=match, ) + self._with_playground = True return self + + + async def _log_before_startup(self): + self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}") + if self._with_graphiql: + self._logger.warning(f"GraphiQL available at http://{self._api_settings.host}:{self._api_settings.port}/api/graphiql") + if self._with_playground: + self._logger.warning(f"GraphQL Playground available at http://{self._api_settings.host}:{self._api_settings.port}/api/playground") \ No newline at end of file diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/auth_user_graph_type.py b/src/cpl-graphql/cpl/graphql/auth/administration/auth_user_graph_type.py deleted file mode 100644 index d96af34e..00000000 --- a/src/cpl-graphql/cpl/graphql/auth/administration/auth_user_graph_type.py +++ /dev/null @@ -1,12 +0,0 @@ -from cpl.auth.schema import AuthUser -from cpl.graphql.schema.db_model_graph_type import DbModelGraphType - - -class AuthUserGraphType(DbModelGraphType): - - def __init__(self): - DbModelGraphType.__init__(self) - - self.string_field(AuthUser.keycloak_id, lambda root: root.keycloak_id) - self.string_field(AuthUser.username, lambda root: root.username) - self.string_field(AuthUser.email, lambda root: root.email) diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/user/__init__.py b/src/cpl-graphql/cpl/graphql/auth/administration/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/auth_user_filter.py b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_filter.py similarity index 80% rename from src/cpl-graphql/cpl/graphql/auth/administration/auth_user_filter.py rename to src/cpl-graphql/cpl/graphql/auth/administration/user/user_filter.py index 19264a46..991e6efb 100644 --- a/src/cpl-graphql/cpl/graphql/auth/administration/auth_user_filter.py +++ b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_filter.py @@ -1,9 +1,9 @@ -from cpl.auth.schema import AuthUser +from cpl.auth.schema import User from cpl.graphql.schema.filter.db_model_filter import DbModelFilter from cpl.graphql.schema.filter.string_filter import StringFilter -class AuthUserFilter(DbModelFilter[AuthUser]): +class UserFilter(DbModelFilter[User]): def __init__(self, public: bool = False): DbModelFilter.__init__(self, public) diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/user/user_graph_type.py b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_graph_type.py new file mode 100644 index 00000000..d27ce05a --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_graph_type.py @@ -0,0 +1,12 @@ +from cpl.auth.schema import User +from cpl.graphql.schema.db_model_graph_type import DbModelGraphType + + +class UserGraphType(DbModelGraphType): + + def __init__(self): + DbModelGraphType.__init__(self) + + self.string_field(User.keycloak_id, lambda root: root.keycloak_id) + self.string_field(User.username, lambda root: root.username) + self.string_field(User.email, lambda root: root.email) diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py new file mode 100644 index 00000000..be46dd10 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py @@ -0,0 +1,23 @@ +from cpl.auth.schema import User +from cpl.core.typing import SerialId +from cpl.graphql.schema.input import Input + + +class UserCreateInput(Input[User]): + keycloak_id: str + roles: list[SerialId] + + def __init__(self): + Input.__init__(self) + self.string_field("keycloak_id").with_required() + self.list_field("roles", SerialId) + + +class UserUpdateInput(Input[User]): + id: SerialId + roles: list[SerialId] + + def __init__(self): + Input.__init__(self) + self.int_field("id").with_required() + self.list_field("roles", SerialId) diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/user/user_mutation.py b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_mutation.py new file mode 100644 index 00000000..c33fd76c --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_mutation.py @@ -0,0 +1,112 @@ +from cpl.api import APILogger +from cpl.auth.keycloak import KeycloakAdmin +from cpl.auth.permission import Permissions +from cpl.auth.schema import UserDao, User, RoleUser, RoleUserDao, RoleDao +from cpl.core.ctx.user_context import get_user +from cpl.graphql.auth.administration.user.user_input import UserCreateInput, UserUpdateInput +from cpl.graphql.schema.mutation import Mutation + + +class UserMutation(Mutation): + def __init__( + self, + logger: APILogger, + user_dao: UserDao, + role_user_dao: RoleUserDao, + role_dao: RoleDao, + keycloak_admin: KeycloakAdmin, + ): + Mutation.__init__(self) + self._logger = logger + self._user_dao = user_dao + self._role_user_dao = role_user_dao + self._role_dao = role_dao + self._keycloak_admin = keycloak_admin + + self.int_field( + "create", + self.resolve_create, + ).with_require_any_permission(Permissions.users_create).with_argument( + "input", + UserCreateInput, + ).with_required() + + self.bool_field( + "update", + self.resolve_update, + ).with_require_any_permission(Permissions.users_update).with_argument( + "input", + UserUpdateInput, + ).with_required() + + self.bool_field( + "delete", + self.resolve_delete, + ).with_require_any_permission(Permissions.users_delete).with_argument( + "id", + int, + ).with_required() + + self.bool_field( + "restore", + self.resolve_restore, + ).with_require_any_permission(Permissions.users_delete).with_argument( + "id", + int, + ).with_required() + + async def resolve_create(self, input: UserCreateInput): + self._logger.debug(f"create user: {input.__dict__}") + + # ensure keycloak knows a user with this keycloak_id + # get_user should raise an exception if the user does not exist + kc_user = self._keycloak_admin.get_user(input.keycloak_id) + if kc_user is None: + raise ValueError(f"Keycloak user with id {input.keycloak_id} does not exist") + + user = User(0, input.keycloak_id, input.license) + user_id = await self._user_dao.create(user) + user = await self._user_dao.get_by_id(user_id) + await self._role_user_dao.create_many([RoleUser(0, user.id, x) for x in set(input.roles)]) + + return user + + async def resolve_update(self, input: UserUpdateInput): + self._logger.debug(f"update user: {input.__dict__}") + user = await self._user_dao.get_by_id(input.id) + + if input.license: + user.license = input.license + + await self._user_dao.update(user) + await self._resolve_assignments( + input.roles or [], + user, + RoleUser.user_id, + RoleUser.role_id, + self._user_dao, + self._role_user_dao, + RoleUser, + self._role_dao, + ) + + return user + + async def resolve_delete(self, id: int): + self._logger.debug(f"delete user: {id}") + user = await self._user_dao.get_by_id(id) + await self._user_dao.delete(user) + try: + active_user = get_user() + if active_user is not None and active_user.id == user.id: + # await broadcast.publish("userLogout", user.id) + self._keycloak_admin.user_logout(user_id=user.keycloak_id) + except Exception as e: + self._logger.error(f"Failed to logout user from Keycloak", e) + return True + + async def resolve_restore(self, id: int): + self._logger.debug(f"restore user: {id}") + user = await self._user_dao.get_by_id(id) + await self._user_dao.restore(user) + return True diff --git a/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py b/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py index a0724910..871676ff 100644 --- a/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py +++ b/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py @@ -1,7 +1,23 @@ +from cpl.core.configuration import Configuration +from cpl.dependency import ServiceProvider from cpl.dependency.module.module import Module -from cpl.graphql.auth.administration.auth_user_filter import AuthUserFilter -from cpl.graphql.auth.administration.auth_user_graph_type import AuthUserGraphType +from cpl.dependency.service_collection import ServiceCollection +from cpl.graphql.auth.administration.user.user_filter import UserFilter +from cpl.graphql.auth.administration.user.user_graph_type import UserGraphType +from cpl.graphql.auth.administration.user.user_mutation import UserMutation +from cpl.graphql.graphql_module import GraphQLModule +from cpl.graphql.service.schema import Schema class GraphQLAuthModule(Module): - transient = [AuthUserGraphType, AuthUserFilter] + dependencies = [GraphQLModule] + transient = [UserGraphType, UserMutation, UserFilter] + + @staticmethod + def register(collection: ServiceCollection): + Configuration.set("GraphQLAuthModuleEnabled", True) + + @staticmethod + def configure(provider: ServiceProvider): + schema = provider.get_service(Schema) + schema.with_type(UserGraphType) \ No newline at end of file diff --git a/src/cpl-graphql/cpl/graphql/graphql_module.py b/src/cpl-graphql/cpl/graphql/graphql_module.py index 05a36787..b749d16e 100644 --- a/src/cpl-graphql/cpl/graphql/graphql_module.py +++ b/src/cpl-graphql/cpl/graphql/graphql_module.py @@ -1,8 +1,6 @@ from cpl.api.api_module import ApiModule -from cpl.dependency import ServiceCollection from cpl.dependency.module.module import Module from cpl.dependency.service_provider import ServiceProvider -from cpl.graphql.auth.graphql_auth_module import GraphQLAuthModule from cpl.graphql.schema.filter.bool_filter import BoolFilter from cpl.graphql.schema.filter.date_filter import DateFilter from cpl.graphql.schema.filter.filter import Filter @@ -20,10 +18,6 @@ class GraphQLModule(Module): scoped = [GraphQLService] transient = [Filter, StringFilter, IntFilter, BoolFilter, DateFilter] - @staticmethod - def register(collection: ServiceCollection): - collection.add_module(GraphQLAuthModule) - @staticmethod def configure(services: ServiceProvider) -> None: schema = services.get_service(Schema) diff --git a/src/cpl-graphql/cpl/graphql/query_context.py b/src/cpl-graphql/cpl/graphql/query_context.py index 44a916ee..831273c4 100644 --- a/src/cpl-graphql/cpl/graphql/query_context.py +++ b/src/cpl-graphql/cpl/graphql/query_context.py @@ -3,7 +3,7 @@ from typing import Optional from graphql import GraphQLResolveInfo -from cpl.auth.schema import AuthUser, Permission +from cpl.auth.schema import User, Permission from cpl.core.ctx import get_user @@ -25,7 +25,7 @@ class QueryContext: self._is_mutation = is_mutation @property - def user(self) -> AuthUser: + def user(self) -> User: return self._user @property diff --git a/src/cpl-graphql/cpl/graphql/schema/db_model_graph_type.py b/src/cpl-graphql/cpl/graphql/schema/db_model_graph_type.py index 32f6cfbc..a2e5ee1f 100644 --- a/src/cpl-graphql/cpl/graphql/schema/db_model_graph_type.py +++ b/src/cpl-graphql/cpl/graphql/schema/db_model_graph_type.py @@ -2,6 +2,7 @@ from typing import Type, Optional, Generic, Annotated import strawberry +from cpl.core.configuration import Configuration from cpl.core.typing import T from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.graphql.schema.graph_type import GraphType @@ -23,9 +24,9 @@ class DbModelGraphType(GraphType[T], Generic[T]): self.int_field("id", lambda root: root.id).with_public(public) self.bool_field("deleted", lambda root: root.deleted).with_public(public) - from cpl.graphql.auth.administration.auth_user_graph_type import AuthUserGraphType - - self.object_field("editor", lambda: AuthUserGraphType, lambda root: root.editor).with_public(public) + if Configuration.get("GraphQLAuthModuleEnabled", False): + from cpl.graphql.auth.administration.user.user_graph_type import UserGraphType + self.object_field("editor", lambda: UserGraphType, lambda root: root.editor).with_public(public) self.string_field("created", lambda root: root.created).with_public(public) self.string_field("updated", lambda root: root.updated).with_public(public) diff --git a/src/cpl-graphql/cpl/graphql/schema/filter/db_model_filter.py b/src/cpl-graphql/cpl/graphql/schema/filter/db_model_filter.py index aa4fb4d8..6e7681a7 100644 --- a/src/cpl-graphql/cpl/graphql/schema/filter/db_model_filter.py +++ b/src/cpl-graphql/cpl/graphql/schema/filter/db_model_filter.py @@ -1,5 +1,6 @@ from typing import Generic +from cpl.core.configuration.configuration import Configuration from cpl.core.typing import T from cpl.graphql.schema.filter.bool_filter import BoolFilter from cpl.graphql.schema.filter.date_filter import DateFilter @@ -13,8 +14,9 @@ class DbModelFilter(Filter[T], Generic[T]): self.field("id", IntFilter).with_public(public) self.field("deleted", BoolFilter).with_public(public) - from cpl.graphql.auth.administration.auth_user_filter import AuthUserFilter + if Configuration.get("GraphQLAuthModuleEnabled", False): + from cpl.graphql.auth.administration.user.user_filter import UserFilter + self.field("editor", lambda: UserFilter).with_public(public) - self.field("editor", lambda: AuthUserFilter).with_public(public) self.field("created", DateFilter).with_public(public) self.field("updated", DateFilter).with_public(public) diff --git a/src/cpl-graphql/cpl/graphql/schema/filter/filter.py b/src/cpl-graphql/cpl/graphql/schema/filter/filter.py index 6463ace9..75bd3c3c 100644 --- a/src/cpl-graphql/cpl/graphql/schema/filter/filter.py +++ b/src/cpl-graphql/cpl/graphql/schema/filter/filter.py @@ -13,16 +13,16 @@ class Filter(Input[T]): Input.__init__(self) def filter_field(self, name: str, filter_type: Type["Filter"]): - self.field(name, filter_type()) + self.field(name, filter_type) def string_field(self, name: str): - self.field(name, StringFilter()) + self.field(name, StringFilter) def int_field(self, name: str): - self.field(name, IntFilter()) + self.field(name, IntFilter) def bool_field(self, name: str): - self.field(name, BoolFilter()) + self.field(name, BoolFilter) def date_field(self, name: str): - self.field(name, DateFilter()) + self.field(name, DateFilter) diff --git a/src/cpl-graphql/cpl/graphql/schema/mutation.py b/src/cpl-graphql/cpl/graphql/schema/mutation.py index 691cee10..82a707e9 100644 --- a/src/cpl-graphql/cpl/graphql/schema/mutation.py +++ b/src/cpl-graphql/cpl/graphql/schema/mutation.py @@ -1,5 +1,7 @@ -from typing import Type +from typing import Type, Union +from cpl.core.typing import T +from cpl.database.abc import DataAccessObjectABC, DbJoinModelABC from cpl.dependency.inject import inject from cpl.dependency.service_provider import ServiceProvider from cpl.graphql.abc.query_abc import QueryABC @@ -23,3 +25,75 @@ class Mutation(QueryABC): raise ValueError(f"Mutation '{cls.__name__}' not registered in service provider") return self.field(name, sub.to_strawberry(), lambda: sub) + + @staticmethod + async def _resolve_assignments( + foreign_objs: list[int], + resolved_obj: T, + reference_key_own: Union[str, property], + reference_key_foreign: Union[str, property], + source_dao: DataAccessObjectABC[T], + join_dao: DataAccessObjectABC[T], + join_type: Type[DbJoinModelABC], + foreign_dao: DataAccessObjectABC[T], + ): + if foreign_objs is None: + return + + reference_key_foreign_attr = reference_key_foreign + if isinstance(reference_key_foreign, property): + reference_key_foreign_attr = reference_key_foreign.fget.__name__ + + foreign_list = await join_dao.find_by( + [{reference_key_own: resolved_obj.id}, {"deleted": False}] + ) + + to_delete = ( + foreign_list + if len(foreign_objs) == 0 + else await join_dao.find_by( + [ + {reference_key_own: resolved_obj.id}, + {reference_key_foreign: {"notIn": foreign_objs}}, + ] + ) + ) + foreign_ids = [getattr(x, reference_key_foreign_attr) for x in foreign_list] + deleted_foreign_ids = [ + getattr(x, reference_key_foreign_attr) + for x in await join_dao.find_by( + [{reference_key_own: resolved_obj.id}, {"deleted": True}] + ) + ] + + to_create = [ + join_type(0, resolved_obj.id, x) + for x in foreign_objs + if x not in foreign_ids and x not in deleted_foreign_ids + ] + to_restore = [ + await join_dao.get_single_by( + [ + {reference_key_own: resolved_obj.id}, + {reference_key_foreign: x}, + ] + ) + for x in foreign_objs + if x not in foreign_ids and x in deleted_foreign_ids + ] + + if len(to_delete) > 0: + await join_dao.delete_many(to_delete) + + if len(to_create) > 0: + await join_dao.create_many(to_create) + + if len(to_restore) > 0: + await join_dao.restore_many(to_restore) + + foreign_changes = [*to_delete, *to_create, *to_restore] + if len(foreign_changes) > 0: + await source_dao.touch(resolved_obj) + await foreign_dao.touch_many_by_id( + [getattr(x, reference_key_foreign_attr) for x in foreign_changes] + ) diff --git a/src/cpl-graphql/cpl/graphql/service/schema.py b/src/cpl-graphql/cpl/graphql/service/schema.py index 3142adaa..9141f455 100644 --- a/src/cpl-graphql/cpl/graphql/service/schema.py +++ b/src/cpl-graphql/cpl/graphql/service/schema.py @@ -6,7 +6,6 @@ import strawberry from cpl.api.logger import APILogger from cpl.dependency.service_provider import ServiceProvider from cpl.graphql.abc.strawberry_protocol import StrawberryProtocol -from cpl.graphql.auth.administration.auth_user_graph_type import AuthUserGraphType from cpl.graphql.schema.root_mutation import RootMutation from cpl.graphql.schema.root_query import RootQuery @@ -17,9 +16,7 @@ class Schema: self._logger = logger self._provider = provider - self._types: dict[str, Type[StrawberryProtocol]] = { - "AuthUserGraphType": AuthUserGraphType, - } + self._types: dict[str, Type[StrawberryProtocol]] = {} self._schema = None