Renamed AuthUsers -> Users & completed user gql #181

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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",
},
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.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]
)

View File

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