Renamed AuthUsers -> Users & completed user gql #181

This commit is contained in:
2025-09-29 08:31:59 +02:00
parent bd13bbca5d
commit f49d1078ff
40 changed files with 387 additions and 150 deletions

View File

@@ -1,17 +1,18 @@
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from api.src.queries.cities import CityGraphType, CityFilter, CitySort 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 api.src.queries.user import UserFilter, UserSort
from cpl.api.api_module import ApiModule from cpl.api.api_module import ApiModule
from cpl.application.application_builder import ApplicationBuilder 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.configuration import Configuration
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.utils.cache import Cache from cpl.core.utils.cache import Cache
from cpl.database.mysql.mysql_module import MySQLModule from cpl.database.mysql.mysql_module import MySQLModule
from cpl.graphql.application.graphql_app import GraphQLApp 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 cpl.graphql.graphql_module import GraphQLModule
from model.author_dao import AuthorDao from model.author_dao import AuthorDao
from model.author_query import AuthorGraphType, AuthorFilter, AuthorSort from model.author_query import AuthorGraphType, AuthorFilter, AuthorSort
@@ -38,8 +39,9 @@ def main():
.add_module(MySQLModule) .add_module(MySQLModule)
.add_module(ApiModule) .add_module(ApiModule)
.add_module(GraphQLModule) .add_module(GraphQLModule)
.add_module(GraphQLAuthModule)
.add_scoped(ScopedService) .add_scoped(ScopedService)
.add_cache(AuthUser) .add_cache(User)
.add_cache(Role) .add_cache(Role)
.add_transient(CityGraphType) .add_transient(CityGraphType)
.add_transient(CityFilter) .add_transient(CityFilter)
@@ -47,9 +49,9 @@ def main():
.add_transient(UserGraphType) .add_transient(UserGraphType)
.add_transient(UserFilter) .add_transient(UserFilter)
.add_transient(UserSort) .add_transient(UserSort)
# .add_transient(AuthUserGraphType) # .add_transient(UserGraphType)
# .add_transient(AuthUserFilter) # .add_transient(UserFilter)
# .add_transient(AuthUserSort) # .add_transient(UserSort)
.add_transient(HelloQuery) .add_transient(HelloQuery)
# test data # test data
.add_singleton(TestDataSeeder) .add_singleton(TestDataSeeder)
@@ -100,7 +102,7 @@ def main():
app.with_permissions(PostPermissions) app.with_permissions(PostPermissions)
provider = builder.service_provider 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]) role_cache = provider.get_service(Cache[Role])
if role_cache == user_cache: if role_cache == user_cache:

View File

@@ -1,7 +1,7 @@
from api.src.queries.cities import CityFilter, CitySort, CityGraphType, City from api.src.queries.cities import CityFilter, CitySort, CityGraphType, City
from api.src.queries.user import User, UserFilter, UserSort, UserGraphType from api.src.queries.user import User, UserFilter, UserSort, UserGraphType
from cpl.api.middleware.request import get_request 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.filter.filter import Filter
from cpl.graphql.schema.graph_type import GraphType from cpl.graphql.schema.graph_type import GraphType
from cpl.graphql.schema.query import Query 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)] users = [User(i, f"User {i}") for i in range(1, 101)]
cities = [City(i, f"City {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): # def __init__(self):
# Filter.__init__(self) # Filter.__init__(self)
# self.field("id", int) # self.field("id", int)
# self.field("username", str) # self.field("username", str)
# #
# #
# class AuthUserSort(Sort[AuthUser]): # class UserSort(Sort[User]):
# def __init__(self): # def __init__(self):
# Sort.__init__(self) # Sort.__init__(self)
# self.field("id", SortOrder) # self.field("id", SortOrder)
# self.field("username", SortOrder) # self.field("username", SortOrder)
# #
# class AuthUserGraphType(GraphType[AuthUser]): # class UserGraphType(GraphType[User]):
# #
# def __init__(self): # def __init__(self):
# GraphType.__init__(self) # GraphType.__init__(self)
@@ -61,9 +61,9 @@ class HelloQuery(Query):
resolver=lambda: cities, resolver=lambda: cities,
) )
# self.dao_collection_field( # self.dao_collection_field(
# AuthUserGraphType, # UserGraphType,
# AuthUserDao, # UserDao,
# "authUsers", # "Users",
# AuthUserFilter, # UserFilter,
# AuthUserSort, # UserSort,
# ) # )

View File

@@ -214,6 +214,9 @@ class WebApp(WebAppABC):
self.with_middleware(AuthorizationMiddleware) self.with_middleware(AuthorizationMiddleware)
return self 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): async def main(self):
self._logger.debug(f"Preparing API") self._logger.debug(f"Preparing API")
self._validate_policies() self._validate_policies()
@@ -237,7 +240,7 @@ class WebApp(WebAppABC):
else: else:
app = self._app 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( config = uvicorn.Config(
app, host=self._api_settings.host, port=self._api_settings.port, log_config=None, loop="asyncio" app, host=self._api_settings.host, port=self._api_settings.port, log_config=None, loop="asyncio"

View File

@@ -7,13 +7,13 @@ from cpl.api.logger import APILogger
from cpl.api.middleware.request import get_request from cpl.api.middleware.request import get_request
from cpl.api.router import Router from cpl.api.router import Router
from cpl.auth.keycloak import KeycloakClient 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 from cpl.core.ctx import set_user
class AuthenticationMiddleware(ASGIMiddleware): 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) ASGIMiddleware.__init__(self, app)
self._logger = logger self._logger = logger
@@ -72,12 +72,12 @@ class AuthenticationMiddleware(ASGIMiddleware):
return await self._call_next(scope, receive, send) 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) existing = await self._user_dao.find_by_keycloak_id(keycloak_id)
if existing is not None: if existing is not None:
return existing return existing
user = AuthUser(0, keycloak_id) user = User(0, keycloak_id)
uid = await self._user_dao.create(user) uid = await self._user_dao.create(user)
return await self._user_dao.get_by_id(uid) return await self._user_dao.get_by_id(uid)

View File

@@ -7,13 +7,13 @@ from cpl.api.middleware.request import get_request
from cpl.api.model.validation_match import ValidationMatch from cpl.api.model.validation_match import ValidationMatch
from cpl.api.registry.policy import PolicyRegistry from cpl.api.registry.policy import PolicyRegistry
from cpl.api.router import Router 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 from cpl.core.ctx.user_context import get_user
class AuthorizationMiddleware(ASGIMiddleware): 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) ASGIMiddleware.__init__(self, app)
self._logger = logger self._logger = logger

View File

@@ -10,8 +10,8 @@ from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.api.typing import TRequest from cpl.api.typing import TRequest
from cpl.auth.keycloak.keycloak_client import KeycloakClient from cpl.auth.keycloak.keycloak_client import KeycloakClient
from cpl.auth.schema import AuthUser from cpl.auth.schema import User
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao from cpl.auth.schema._administration.user_dao import UserDao
from cpl.core.ctx import set_user from cpl.core.ctx import set_user
from cpl.dependency.inject import inject from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
@@ -22,7 +22,7 @@ _request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", defa
class RequestMiddleware(ASGIMiddleware): class RequestMiddleware(ASGIMiddleware):
def __init__( 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) ASGIMiddleware.__init__(self, app)
@@ -80,7 +80,7 @@ class RequestMiddleware(ASGIMiddleware):
user = await self._user_dao.find_by_keycloak_id(keycloak_id) user = await self._user_dao.find_by_keycloak_id(keycloak_id)
if not user: if not user:
user = AuthUser(0, keycloak_id) user = User(0, keycloak_id)
uid = await self._user_dao.create(user) uid = await self._user_dao.create(user)
user = await self._user_dao.get_by_id(uid) user = await self._user_dao.get_by_id(uid)

View File

@@ -7,7 +7,7 @@ from starlette.types import ASGIApp
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware 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] TRequest = Union[Request, WebSocket]
TEndpoint = Callable[[TRequest, ...], Awaitable[Response]] | Callable[[TRequest, ...], Response] TEndpoint = Callable[[TRequest, ...], Awaitable[Response]] | Callable[[TRequest, ...], Response]
@@ -18,5 +18,5 @@ PartialMiddleware = Union[
Middleware, Middleware,
Callable[[ASGIApp], ASGIApp], Callable[[ASGIApp], ASGIApp],
] ]
PolicyResolver = Callable[[AuthUser], bool | Awaitable[bool]] PolicyResolver = Callable[[User], bool | Awaitable[bool]]
PolicyInput = Union[dict[str, PolicyResolver], "Policy"] PolicyInput = Union[dict[str, PolicyResolver], "Policy"]

View File

@@ -12,7 +12,7 @@ from cpl.dependency.service_provider import ServiceProvider
from .keycloak.keycloak_admin import KeycloakAdmin from .keycloak.keycloak_admin import KeycloakAdmin
from .keycloak.keycloak_client import KeycloakClient from .keycloak.keycloak_client import KeycloakClient
from .schema._administration.api_key_dao import ApiKeyDao 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.api_key_permission_dao import ApiKeyPermissionDao
from .schema._permission.permission_dao import PermissionDao from .schema._permission.permission_dao import PermissionDao
from .schema._permission.role_dao import RoleDao from .schema._permission.role_dao import RoleDao
@@ -26,7 +26,7 @@ class AuthModule(Module):
singleton = [ singleton = [
KeycloakClient, KeycloakClient,
KeycloakAdmin, KeycloakAdmin,
AuthUserDao, UserDao,
ApiKeyDao, ApiKeyDao,
ApiKeyPermissionDao, ApiKeyPermissionDao,
PermissionDao, PermissionDao,

View File

@@ -6,7 +6,7 @@ from cpl.auth.schema import (
RolePermissionDao, RolePermissionDao,
ApiKeyDao, ApiKeyDao,
ApiKeyPermissionDao, ApiKeyPermissionDao,
AuthUserDao, UserDao,
RoleUserDao, RoleUserDao,
RoleUser, RoleUser,
) )
@@ -23,7 +23,7 @@ class RoleSeeder(DataSeederABC):
role_permission_dao: RolePermissionDao, role_permission_dao: RolePermissionDao,
api_key_dao: ApiKeyDao, api_key_dao: ApiKeyDao,
api_key_permission_dao: ApiKeyPermissionDao, api_key_permission_dao: ApiKeyPermissionDao,
user_dao: AuthUserDao, user_dao: UserDao,
role_user_dao: RoleUserDao, role_user_dao: RoleUserDao,
): ):
DataSeederABC.__init__(self) DataSeederABC.__init__(self)

View File

@@ -1,7 +1,7 @@
from ._administration.api_key import ApiKey from ._administration.api_key import ApiKey
from ._administration.api_key_dao import ApiKeyDao from ._administration.api_key_dao import ApiKeyDao
from ._administration.auth_user import AuthUser from ._administration.user import User
from ._administration.auth_user_dao import AuthUserDao from ._administration.user_dao import UserDao
from ._permission.api_key_permission import ApiKeyPermission from ._permission.api_key_permission import ApiKeyPermission
from ._permission.api_key_permission_dao import ApiKeyPermissionDao from ._permission.api_key_permission_dao import ApiKeyPermissionDao

View File

@@ -13,7 +13,7 @@ from cpl.database.logger import DBLogger
from cpl.dependency import get_provider from cpl.dependency import get_provider
class AuthUser(DbModelABC[Self]): class User(DbModelABC[Self]):
def __init__( def __init__(
self, self,
id: SerialId, id: SerialId,
@@ -69,21 +69,21 @@ class AuthUser(DbModelABC[Self]):
@async_property @async_property
async def permissions(self): 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) user_dao: UserDao = get_provider().get_service(UserDao)
return await auth_user_dao.get_permissions(self.id) return await user_dao.get_permissions(self.id)
async def has_permission(self, permission: Permissions) -> bool: 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) user_dao: UserDao = get_provider().get_service(UserDao)
return await auth_user_dao.has_permission(self.id, permission) return await user_dao.has_permission(self.id, permission)
async def anonymize(self): 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)) self._keycloak_id = str(uuid.UUID(int=0))
await auth_user_dao.update(self) await user_dao.update(self)

View File

@@ -3,21 +3,21 @@ from typing import Optional, Union
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.auth.schema._permission.permission_dao import PermissionDao from cpl.auth.schema._permission.permission_dao import PermissionDao
from cpl.auth.schema._permission.permission import Permission 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 import TableManager
from cpl.database.abc import DbModelDaoABC from cpl.database.abc import DbModelDaoABC
from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder
from cpl.dependency.context import get_provider from cpl.dependency.context import get_provider
class AuthUserDao(DbModelDaoABC[AuthUser]): class UserDao(DbModelDaoABC[User]):
def __init__(self, permission_dao: PermissionDao): 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._permissions = permission_dao
self.attribute(AuthUser.keycloak_id, str) self.attribute(User.keycloak_id, str)
async def get_users(): async def get_users():
return [(x.id, x.username, x.email) for x in await self.get_all()] 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) .with_value_getter(get_users)
) )
async def get_by_keycloak_id(self, keycloak_id: str) -> AuthUser: async def get_by_keycloak_id(self, keycloak_id: str) -> User:
return await self.get_single_by({AuthUser.keycloak_id: keycloak_id}) return await self.get_single_by({User.keycloak_id: keycloak_id})
async def find_by_keycloak_id(self, keycloak_id: str) -> Optional[AuthUser]: async def find_by_keycloak_id(self, keycloak_id: str) -> Optional[User]:
return await self.find_single_by({AuthUser.keycloak_id: keycloak_id}) return await self.find_single_by({User.keycloak_id: keycloak_id})
async def has_permission(self, user_id: int, permission: Union[Permissions, str]) -> bool: async def has_permission(self, user_id: int, permission: Union[Permissions, str]) -> bool:
from cpl.auth.schema._permission.permission_dao import PermissionDao from cpl.auth.schema._permission.permission_dao import PermissionDao

View File

@@ -29,10 +29,10 @@ class RoleUser(DbJoinModelABC):
@async_property @async_property
async def user(self): 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) user_dao: UserDao = get_provider().get_service(UserDao)
return await auth_user_dao.get_by_id(self._user_id) return await user_dao.get_by_id(self._user_id)
@property @property
def role_id(self) -> int: def role_id(self) -> int:

View File

@@ -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, id INT AUTO_INCREMENT PRIMARY KEY,
keycloakId CHAR(36) NOT NULL, 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, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT UC_KeycloakId UNIQUE (keycloakId), 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, id INT NOT NULL,
keycloakId CHAR(36) NOT NULL, keycloakId CHAR(36) NOT NULL,
@@ -23,22 +23,22 @@ CREATE TABLE IF NOT EXISTS administration_auth_users_history
updated TIMESTAMP NOT NULL updated TIMESTAMP NOT NULL
); );
CREATE TRIGGER TR_administration_auth_usersUpdate CREATE TRIGGER TR_administration_usersUpdate
AFTER UPDATE AFTER UPDATE
ON administration_auth_users ON administration_users
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
INSERT INTO administration_auth_users_history INSERT INTO administration_users_history
(id, keycloakId, deleted, editorId, created, updated) (id, keycloakId, deleted, editorId, created, updated)
VALUES (OLD.id, OLD.keycloakId, OLD.deleted, OLD.editorId, OLD.created, NOW()); VALUES (OLD.id, OLD.keycloakId, OLD.deleted, OLD.editorId, OLD.created, NOW());
END; END;
CREATE TRIGGER TR_administration_auth_usersDelete CREATE TRIGGER TR_administration_usersDelete
AFTER DELETE AFTER DELETE
ON administration_auth_users ON administration_users
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
INSERT INTO administration_auth_users_history INSERT INTO administration_users_history
(id, keycloakId, deleted, editorId, created, updated) (id, keycloakId, deleted, editorId, created, updated)
VALUES (OLD.id, OLD.keycloakId, 1, OLD.editorId, OLD.created, NOW()); VALUES (OLD.id, OLD.keycloakId, 1, OLD.editorId, OLD.created, NOW());
END; END;

View File

@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS administration_api_keys
CONSTRAINT UC_Identifier_Key UNIQUE (identifier, keyString), CONSTRAINT UC_Identifier_Key UNIQUE (identifier, keyString),
CONSTRAINT UC_Key UNIQUE (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 CREATE TABLE IF NOT EXISTS administration_api_keys_history

View File

@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS permission_permissions
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT UQ_PermissionName UNIQUE (name), 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 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, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT UQ_RoleName UNIQUE (name), 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 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 UQ_RolePermission UNIQUE (roleId, permissionId),
CONSTRAINT FK_RolePermissions_Role FOREIGN KEY (roleId) REFERENCES permission_roles (id) ON DELETE CASCADE, 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_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 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()); VALUES (OLD.id, OLD.roleId, OLD.permissionId, 1, OLD.editorId, OLD.created, NOW());
END; END;
CREATE TABLE IF NOT EXISTS permission_role_auth_users CREATE TABLE IF NOT EXISTS permission_role_users
( (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
roleId INT NOT NULL, roleId INT NOT NULL,
@@ -142,12 +142,12 @@ CREATE TABLE IF NOT EXISTS permission_role_auth_users
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT UQ_RoleUser UNIQUE (roleId, userId), CONSTRAINT UQ_RoleUser UNIQUE (roleId, userId),
CONSTRAINT FK_Roleauth_users_Role FOREIGN KEY (roleId) REFERENCES permission_roles (id) ON DELETE CASCADE, CONSTRAINT FK_Roleusers_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_Roleusers_User FOREIGN KEY (userId) REFERENCES administration_users (id) ON DELETE CASCADE,
CONSTRAINT FK_Roleauth_users_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) 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, id INT NOT NULL,
roleId 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 updated TIMESTAMP NOT NULL
); );
CREATE TRIGGER TR_Roleauth_usersUpdate CREATE TRIGGER TR_RoleusersUpdate
AFTER UPDATE AFTER UPDATE
ON permission_role_auth_users ON permission_role_users
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
INSERT INTO permission_role_auth_users_history INSERT INTO permission_role_users_history
(id, roleId, userId, deleted, editorId, created, updated) (id, roleId, userId, deleted, editorId, created, updated)
VALUES (OLD.id, OLD.roleId, OLD.userId, OLD.deleted, OLD.editorId, OLD.created, NOW()); VALUES (OLD.id, OLD.roleId, OLD.userId, OLD.deleted, OLD.editorId, OLD.created, NOW());
END; END;
CREATE TRIGGER TR_Roleauth_usersDelete CREATE TRIGGER TR_RoleusersDelete
AFTER DELETE AFTER DELETE
ON permission_role_auth_users ON permission_role_users
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
INSERT INTO permission_role_auth_users_history INSERT INTO permission_role_users_history
(id, roleId, userId, deleted, editorId, created, updated) (id, roleId, userId, deleted, editorId, created, updated)
VALUES (OLD.id, OLD.roleId, OLD.userId, 1, OLD.editorId, OLD.created, NOW()); VALUES (OLD.id, OLD.roleId, OLD.userId, 1, OLD.editorId, OLD.created, NOW());
END; END;

View File

@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS permission_api_key_permissions
CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId), CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId),
CONSTRAINT FK_ApiKeyPermissions_ApiKey FOREIGN KEY (apiKeyId) REFERENCES administration_api_keys (id) ON DELETE CASCADE, 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_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 CREATE TABLE IF NOT EXISTS permission_api_key_permissions_history

View File

@@ -1,26 +1,26 @@
CREATE SCHEMA IF NOT EXISTS administration; 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, id SERIAL PRIMARY KEY,
keycloakId UUID NOT NULL, keycloakId UUID NOT NULL,
-- for history -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UC_KeycloakId UNIQUE (keycloakId) 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 CREATE TRIGGER users_history_trigger
BEFORE INSERT OR UPDATE OR DELETE BEFORE INSERT OR UPDATE OR DELETE
ON administration.auth_users ON administration.users
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION public.history_trigger_function(); EXECUTE FUNCTION public.history_trigger_function();

View File

@@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS administration.api_keys
keyString VARCHAR(255) NOT NULL, keyString VARCHAR(255) NOT NULL,
-- for history -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),

View File

@@ -9,7 +9,7 @@ CREATE TABLE permission.permissions
-- for history -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_PermissionName UNIQUE (name) CONSTRAINT UQ_PermissionName UNIQUE (name)
@@ -35,7 +35,7 @@ CREATE TABLE permission.roles
-- for history -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_RoleName UNIQUE (name) CONSTRAINT UQ_RoleName UNIQUE (name)
@@ -61,7 +61,7 @@ CREATE TABLE permission.role_permissions
-- for history -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_RolePermission UNIQUE (RoleId, permissionId) CONSTRAINT UQ_RolePermission UNIQUE (RoleId, permissionId)
@@ -79,27 +79,27 @@ CREATE TRIGGER versioning_trigger
EXECUTE PROCEDURE public.history_trigger_function(); EXECUTE PROCEDURE public.history_trigger_function();
-- Role user -- Role user
CREATE TABLE permission.role_auth_users CREATE TABLE permission.role_users
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
RoleId INT NOT NULL REFERENCES permission.roles (id) ON DELETE CASCADE, 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 -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_RoleUser UNIQUE (RoleId, UserId) 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 CREATE TRIGGER versioning_trigger
BEFORE INSERT OR UPDATE OR DELETE BEFORE INSERT OR UPDATE OR DELETE
ON permission.role_auth_users ON permission.role_users
FOR EACH ROW FOR EACH ROW
EXECUTE PROCEDURE public.history_trigger_function(); EXECUTE PROCEDURE public.history_trigger_function();

View File

@@ -6,7 +6,7 @@ CREATE TABLE permission.api_key_permissions
-- for history -- for history
deleted BOOLEAN NOT NULL DEFAULT FALSE, 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(), created timestamptz NOT NULL DEFAULT NOW(),
updated timestamptz NOT NULL DEFAULT NOW(), updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId) CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId)

View File

@@ -1,13 +1,13 @@
from contextvars import ContextVar from contextvars import ContextVar
from typing import Optional 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 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 from cpl.core.log.logger_abc import LoggerABC
logger = get_provider().get_service(LoggerABC) logger = get_provider().get_service(LoggerABC)
@@ -15,5 +15,5 @@ def set_user(user: Optional[AuthUser]):
_user_context.set(user) _user_context.set(user)
def get_user() -> Optional[AuthUser]: def get_user() -> Optional[User]:
return _user_context.get() return _user_context.get()

View File

@@ -49,11 +49,11 @@ class DbModelABC(ABC, Generic[T]):
if self._editor_id is None: if self._editor_id is None:
return 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 @property
def created(self) -> datetime: def created(self) -> datetime:

View File

@@ -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.attribute(DbModelABC.editor_id, int, db_name="editorId", ignore=True) # handled by db trigger
self.reference( 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 ) # not relevant for updates due to editor_id
self.attribute(DbModelABC.created, datetime, ignore=True) # handled by db trigger self.attribute(DbModelABC.created, datetime, ignore=True) # handled by db trigger

View File

@@ -7,9 +7,9 @@ class TableManager:
ServerTypes.POSTGRES: "system._executed_migrations", ServerTypes.POSTGRES: "system._executed_migrations",
ServerTypes.MYSQL: "system__executed_migrations", ServerTypes.MYSQL: "system__executed_migrations",
}, },
"auth_users": { "users": {
ServerTypes.POSTGRES: "administration.auth_users", ServerTypes.POSTGRES: "administration.users",
ServerTypes.MYSQL: "administration_auth_users", ServerTypes.MYSQL: "administration_users",
}, },
"api_keys": { "api_keys": {
ServerTypes.POSTGRES: "administration.api_keys", ServerTypes.POSTGRES: "administration.api_keys",
@@ -32,8 +32,8 @@ class TableManager:
ServerTypes.MYSQL: "permission_role_permissions", ServerTypes.MYSQL: "permission_role_permissions",
}, },
"role_users": { "role_users": {
ServerTypes.POSTGRES: "permission.role_auth_users", ServerTypes.POSTGRES: "permission.role_users",
ServerTypes.MYSQL: "permission_role_auth_users", ServerTypes.MYSQL: "permission_role_users",
}, },
} }

View File

@@ -16,6 +16,9 @@ class GraphQLApp(WebApp):
def __init__(self, services: ServiceProvider, modules: Modules): def __init__(self, services: ServiceProvider, modules: Modules):
WebApp.__init__(self, services, modules, [GraphQLModule]) WebApp.__init__(self, services, modules, [GraphQLModule])
self._with_graphiql = False
self._with_playground = False
def with_graphql( def with_graphql(
self, self,
authentication: bool = False, authentication: bool = False,
@@ -57,6 +60,7 @@ class GraphQLApp(WebApp):
policies=policies, policies=policies,
match=match, match=match,
) )
self._with_graphiql = True
return self return self
def with_playground( def with_playground(
@@ -77,4 +81,13 @@ class GraphQLApp(WebApp):
policies=policies, policies=policies,
match=match, match=match,
) )
self._with_playground = True
return self 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")

View File

@@ -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)

View File

@@ -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.db_model_filter import DbModelFilter
from cpl.graphql.schema.filter.string_filter import StringFilter from cpl.graphql.schema.filter.string_filter import StringFilter
class AuthUserFilter(DbModelFilter[AuthUser]): class UserFilter(DbModelFilter[User]):
def __init__(self, public: bool = False): def __init__(self, public: bool = False):
DbModelFilter.__init__(self, public) DbModelFilter.__init__(self, public)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -1,7 +1,23 @@
from cpl.core.configuration import Configuration
from cpl.dependency import ServiceProvider
from cpl.dependency.module.module import Module from cpl.dependency.module.module import Module
from cpl.graphql.auth.administration.auth_user_filter import AuthUserFilter from cpl.dependency.service_collection import ServiceCollection
from cpl.graphql.auth.administration.auth_user_graph_type import AuthUserGraphType 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): 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)

View File

@@ -1,8 +1,6 @@
from cpl.api.api_module import ApiModule from cpl.api.api_module import ApiModule
from cpl.dependency import ServiceCollection
from cpl.dependency.module.module import Module from cpl.dependency.module.module import Module
from cpl.dependency.service_provider import ServiceProvider 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.bool_filter import BoolFilter
from cpl.graphql.schema.filter.date_filter import DateFilter from cpl.graphql.schema.filter.date_filter import DateFilter
from cpl.graphql.schema.filter.filter import Filter from cpl.graphql.schema.filter.filter import Filter
@@ -20,10 +18,6 @@ class GraphQLModule(Module):
scoped = [GraphQLService] scoped = [GraphQLService]
transient = [Filter, StringFilter, IntFilter, BoolFilter, DateFilter] transient = [Filter, StringFilter, IntFilter, BoolFilter, DateFilter]
@staticmethod
def register(collection: ServiceCollection):
collection.add_module(GraphQLAuthModule)
@staticmethod @staticmethod
def configure(services: ServiceProvider) -> None: def configure(services: ServiceProvider) -> None:
schema = services.get_service(Schema) schema = services.get_service(Schema)

View File

@@ -3,7 +3,7 @@ from typing import Optional
from graphql import GraphQLResolveInfo 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 from cpl.core.ctx import get_user
@@ -25,7 +25,7 @@ class QueryContext:
self._is_mutation = is_mutation self._is_mutation = is_mutation
@property @property
def user(self) -> AuthUser: def user(self) -> User:
return self._user return self._user
@property @property

View File

@@ -2,6 +2,7 @@ from typing import Type, Optional, Generic, Annotated
import strawberry import strawberry
from cpl.core.configuration import Configuration
from cpl.core.typing import T from cpl.core.typing import T
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
from cpl.graphql.schema.graph_type import GraphType 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.int_field("id", lambda root: root.id).with_public(public)
self.bool_field("deleted", lambda root: root.deleted).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 if Configuration.get("GraphQLAuthModuleEnabled", False):
from cpl.graphql.auth.administration.user.user_graph_type import UserGraphType
self.object_field("editor", lambda: AuthUserGraphType, lambda root: root.editor).with_public(public) 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("created", lambda root: root.created).with_public(public)
self.string_field("updated", lambda root: root.updated).with_public(public) self.string_field("updated", lambda root: root.updated).with_public(public)

View File

@@ -1,5 +1,6 @@
from typing import Generic from typing import Generic
from cpl.core.configuration.configuration import Configuration
from cpl.core.typing import T from cpl.core.typing import T
from cpl.graphql.schema.filter.bool_filter import BoolFilter from cpl.graphql.schema.filter.bool_filter import BoolFilter
from cpl.graphql.schema.filter.date_filter import DateFilter 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("id", IntFilter).with_public(public)
self.field("deleted", BoolFilter).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("created", DateFilter).with_public(public)
self.field("updated", DateFilter).with_public(public) self.field("updated", DateFilter).with_public(public)

View File

@@ -13,16 +13,16 @@ class Filter(Input[T]):
Input.__init__(self) Input.__init__(self)
def filter_field(self, name: str, filter_type: Type["Filter"]): 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): def string_field(self, name: str):
self.field(name, StringFilter()) self.field(name, StringFilter)
def int_field(self, name: str): def int_field(self, name: str):
self.field(name, IntFilter()) self.field(name, IntFilter)
def bool_field(self, name: str): def bool_field(self, name: str):
self.field(name, BoolFilter()) self.field(name, BoolFilter)
def date_field(self, name: str): def date_field(self, name: str):
self.field(name, DateFilter()) self.field(name, DateFilter)

View File

@@ -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.inject import inject
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.graphql.abc.query_abc import QueryABC 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") raise ValueError(f"Mutation '{cls.__name__}' not registered in service provider")
return self.field(name, sub.to_strawberry(), lambda: sub) 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]
)

View File

@@ -6,7 +6,6 @@ import strawberry
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.graphql.abc.strawberry_protocol import StrawberryProtocol 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_mutation import RootMutation
from cpl.graphql.schema.root_query import RootQuery from cpl.graphql.schema.root_query import RootQuery
@@ -17,9 +16,7 @@ class Schema:
self._logger = logger self._logger = logger
self._provider = provider self._provider = provider
self._types: dict[str, Type[StrawberryProtocol]] = { self._types: dict[str, Type[StrawberryProtocol]] = {}
"AuthUserGraphType": AuthUserGraphType,
}
self._schema = None self._schema = None