From 504dc5e188db6e371473e4a83b83012023bb0808 Mon Sep 17 00:00:00 2001 From: edraft Date: Wed, 17 Sep 2025 12:21:32 +0200 Subject: [PATCH] Added auth & improved database --- .gitea/workflows/build-dev.yaml | 44 ++++- src/cpl-auth/cpl/auth/__init__.py | 70 +++++++ src/cpl-auth/cpl/auth/auth_logger.py | 8 + src/cpl-auth/cpl/auth/keycloak/__init__.py | 3 + .../cpl/auth/keycloak/keycloak_admin.py | 24 +++ .../cpl/auth/keycloak/keycloak_client.py | 26 +++ .../cpl/auth/keycloak/keycloak_user.py | 36 ++++ src/cpl-auth/cpl/auth/keycloak_settings.py | 37 ++++ src/cpl-auth/cpl/auth/permission/__init__.py | 0 .../cpl/auth/permission/permissions.py | 36 ++++ .../auth/permission/permissions_registry.py | 24 +++ src/cpl-auth/cpl/auth/permission_seeder.py | 120 ++++++++++++ src/cpl-auth/cpl/auth/schema/__init__.py | 15 ++ .../auth/schema/_administration/__init__.py | 0 .../auth/schema/_administration/api_key.py | 59 ++++++ .../schema/_administration/api_key_dao.py | 32 ++++ .../auth/schema/_administration/auth_user.py | 89 +++++++++ .../schema/_administration/auth_user_dao.py | 72 +++++++ .../cpl/auth/schema/_permission/__init__.py | 0 .../schema/_permission/api_key_permission.py | 46 +++++ .../_permission/api_key_permission_dao.py | 29 +++ .../cpl/auth/schema/_permission/permission.py | 37 ++++ .../auth/schema/_permission/permission_dao.py | 21 ++ .../cpl/auth/schema/_permission/role.py | 66 +++++++ .../cpl/auth/schema/_permission/role_dao.py | 17 ++ .../schema/_permission/role_permission.py | 46 +++++ .../schema/_permission/role_permission_dao.py | 29 +++ .../cpl/auth/schema/_permission/role_user.py | 46 +++++ .../auth/schema/_permission/role_user_dao.py | 29 +++ .../cpl/auth/scripts/mysql/1-users.sql | 44 +++++ .../cpl/auth/scripts/mysql/2-api-key.sql | 46 +++++ .../scripts/mysql/3-roles-permissions.sql | 179 ++++++++++++++++++ .../scripts/mysql/4-api-key-permissions.sql | 46 +++++ .../cpl/auth/scripts/postgres/1-users.sql | 26 +++ .../cpl/auth/scripts/postgres/2-api-key.sql | 28 +++ .../scripts/postgres/3-roles-permissions.sql | 105 ++++++++++ .../postgres/4-api-key-permissions.sql | 24 +++ src/cpl-auth/pyproject.toml | 30 +++ src/cpl-auth/requirements.dev.txt | 1 + src/cpl-auth/requirements.txt | 4 + src/cpl-core/cpl/core/__init__.py | 1 - src/cpl-core/cpl/core/ctx/__init__.py | 1 + src/cpl-core/cpl/core/ctx/user_context.py | 18 ++ src/cpl-database/cpl/database/__init__.py | 10 +- .../database/abc/data_access_object_abc.py | 13 +- .../cpl/database/abc/data_seeder_abc.py | 8 + .../cpl/database/abc/db_model_dao_abc.py | 6 +- .../cpl/database/abc/table_abc.py | 37 ---- ...py => external_data_temp_table_builder.py} | 0 .../cpl/database/internal_tables.py | 15 -- .../cpl/database/model/server_type.py | 3 +- .../cpl/database/mysql/mysql_pool.py | 2 +- .../database/postgres/sql_select_builder.py | 2 +- .../database/schema/executed_migration_dao.py | 4 +- .../database/scripts/mysql/0-cpl-initial.sql | 2 +- .../cpl/database/scripts/mysql/trigger.txt | 33 ++-- .../scripts/postgres/0-cpl-initial.sql | 10 +- .../cpl/database/service/seeder_service.py | 18 ++ .../cpl/database/table_manager.py | 49 +++++ .../cpl/dependency/service_provider.py | 9 +- .../cpl/dependency/service_provider_abc.py | 14 +- src/cpl-mail/requirements.txt | 3 +- src/cpl-translation/requirements.txt | 3 +- tests/custom/database/src/application.py | 31 +-- .../custom/database/src/custom_permissions.py | 5 + tests/custom/database/src/main.py | 3 +- tests/custom/database/src/model/city.py | 29 +++ tests/custom/database/src/model/city_dao.py | 11 ++ tests/custom/database/src/model/city_model.py | 54 ------ tests/custom/database/src/model/user.py | 20 +- tests/custom/database/src/model/user_dao.py | 3 +- tests/custom/database/src/model/user_model.py | 56 ------ tests/custom/database/src/model/user_repo.py | 38 ---- .../database/src/model/user_repo_abc.py | 22 --- .../custom/database/src/scripts/0-initial.sql | 8 + tests/custom/database/src/startup.py | 16 +- 76 files changed, 1849 insertions(+), 302 deletions(-) create mode 100644 src/cpl-auth/cpl/auth/__init__.py create mode 100644 src/cpl-auth/cpl/auth/auth_logger.py create mode 100644 src/cpl-auth/cpl/auth/keycloak/__init__.py create mode 100644 src/cpl-auth/cpl/auth/keycloak/keycloak_admin.py create mode 100644 src/cpl-auth/cpl/auth/keycloak/keycloak_client.py create mode 100644 src/cpl-auth/cpl/auth/keycloak/keycloak_user.py create mode 100644 src/cpl-auth/cpl/auth/keycloak_settings.py create mode 100644 src/cpl-auth/cpl/auth/permission/__init__.py create mode 100644 src/cpl-auth/cpl/auth/permission/permissions.py create mode 100644 src/cpl-auth/cpl/auth/permission/permissions_registry.py create mode 100644 src/cpl-auth/cpl/auth/permission_seeder.py create mode 100644 src/cpl-auth/cpl/auth/schema/__init__.py create mode 100644 src/cpl-auth/cpl/auth/schema/_administration/__init__.py create mode 100644 src/cpl-auth/cpl/auth/schema/_administration/api_key.py create mode 100644 src/cpl-auth/cpl/auth/schema/_administration/api_key_dao.py create mode 100644 src/cpl-auth/cpl/auth/schema/_administration/auth_user.py create mode 100644 src/cpl-auth/cpl/auth/schema/_administration/auth_user_dao.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/__init__.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/api_key_permission.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/api_key_permission_dao.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/permission.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/permission_dao.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/role.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/role_dao.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/role_permission.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/role_permission_dao.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/role_user.py create mode 100644 src/cpl-auth/cpl/auth/schema/_permission/role_user_dao.py create mode 100644 src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/mysql/2-api-key.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/mysql/3-roles-permissions.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/mysql/4-api-key-permissions.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/postgres/2-api-key.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/postgres/3-roles-permissions.sql create mode 100644 src/cpl-auth/cpl/auth/scripts/postgres/4-api-key-permissions.sql create mode 100644 src/cpl-auth/pyproject.toml create mode 100644 src/cpl-auth/requirements.dev.txt create mode 100644 src/cpl-auth/requirements.txt create mode 100644 src/cpl-core/cpl/core/ctx/__init__.py create mode 100644 src/cpl-core/cpl/core/ctx/user_context.py create mode 100644 src/cpl-database/cpl/database/abc/data_seeder_abc.py delete mode 100644 src/cpl-database/cpl/database/abc/table_abc.py rename src/cpl-database/cpl/database/{_external_data_temp_table_builder.py => external_data_temp_table_builder.py} (100%) delete mode 100644 src/cpl-database/cpl/database/internal_tables.py create mode 100644 src/cpl-database/cpl/database/service/seeder_service.py create mode 100644 src/cpl-database/cpl/database/table_manager.py create mode 100644 tests/custom/database/src/custom_permissions.py create mode 100644 tests/custom/database/src/model/city.py create mode 100644 tests/custom/database/src/model/city_dao.py delete mode 100644 tests/custom/database/src/model/city_model.py delete mode 100644 tests/custom/database/src/model/user_model.py delete mode 100644 tests/custom/database/src/model/user_repo.py delete mode 100644 tests/custom/database/src/model/user_repo_abc.py diff --git a/.gitea/workflows/build-dev.yaml b/.gitea/workflows/build-dev.yaml index b6454e2a..71d9b284 100644 --- a/.gitea/workflows/build-dev.yaml +++ b/.gitea/workflows/build-dev.yaml @@ -12,6 +12,20 @@ jobs: version_suffix: 'dev' secrets: inherit + application: + uses: ./.gitea/workflows/package.yaml + needs: [ prepare, core, dependency ] + with: + working_directory: src/cpl-application + secrets: inherit + + auth: + uses: ./.gitea/workflows/package.yaml + needs: [ prepare, core, dependency, database ] + with: + working_directory: src/cpl-auth + secrets: inherit + core: uses: ./.gitea/workflows/package.yaml needs: [prepare] @@ -19,6 +33,27 @@ jobs: working_directory: src/cpl-core secrets: inherit + database: + uses: ./.gitea/workflows/package.yaml + needs: [ prepare, core, dependency ] + with: + working_directory: src/cpl-database + secrets: inherit + + dependency: + uses: ./.gitea/workflows/package.yaml + needs: [ prepare, core ] + with: + working_directory: src/cpl-dependency + secrets: inherit + + mail: + uses: ./.gitea/workflows/package.yaml + needs: [ prepare, core, dependency ] + with: + working_directory: src/cpl-mail + secrets: inherit + query: uses: ./.gitea/workflows/package.yaml needs: [prepare] @@ -28,14 +63,7 @@ jobs: translation: uses: ./.gitea/workflows/package.yaml - needs: [ prepare, core ] + needs: [ prepare, core, dependency ] with: working_directory: src/cpl-translation - secrets: inherit - - mail: - uses: ./.gitea/workflows/package.yaml - needs: [ prepare, core ] - with: - working_directory: src/cpl-mail secrets: inherit \ No newline at end of file diff --git a/src/cpl-auth/cpl/auth/__init__.py b/src/cpl-auth/cpl/auth/__init__.py new file mode 100644 index 00000000..f5147636 --- /dev/null +++ b/src/cpl-auth/cpl/auth/__init__.py @@ -0,0 +1,70 @@ +from cpl.auth import permission as _permission +from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin +from cpl.auth.keycloak.keycloak_client import KeycloakClient +from cpl.dependency import ServiceCollection as _ServiceCollection + +from .auth_logger import AuthLogger +from .keycloak_settings import KeycloakSettings +from .permission_seeder import PermissionSeeder + + +def _add_daos(collection: _ServiceCollection): + from .schema._administration.auth_user_dao import AuthUserDao + from .schema._administration.api_key_dao import ApiKeyDao + from .schema._permission.api_key_permission_dao import ApiKeyPermissionDao + from .schema._permission.permission_dao import PermissionDao + from .schema._permission.role_dao import RoleDao + from .schema._permission.role_permission_dao import RolePermissionDao + from .schema._permission.role_user_dao import RoleUserDao + + collection.add_singleton(AuthUserDao) + collection.add_singleton(ApiKeyDao) + collection.add_singleton(ApiKeyPermissionDao) + collection.add_singleton(PermissionDao) + collection.add_singleton(RoleDao) + collection.add_singleton(RolePermissionDao) + collection.add_singleton(RoleUserDao) + + +def add_auth(collection: _ServiceCollection): + import os + + from cpl.core.console import Console + from cpl.database.service.migration_service import MigrationService + from cpl.database.model.server_type import ServerType, ServerTypes + + try: + collection.add_singleton(KeycloakClient) + collection.add_singleton(KeycloakAdmin) + + _add_daos(collection) + + provider = collection.build_service_provider() + migration_service: MigrationService = provider.get_service(MigrationService) + if ServerType.server_type == ServerTypes.POSTGRES: + migration_service.with_directory( + os.path.join(os.path.dirname(os.path.realpath(__file__)), "scripts/postgres") + ) + elif ServerType.server_type == ServerTypes.MYSQL: + migration_service.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "scripts/mysql")) + except ImportError as e: + Console.error("cpl-auth is not installed", str(e)) + + +def add_permission(collection: _ServiceCollection): + from cpl.auth.permission_seeder import PermissionSeeder + from cpl.database.abc.data_seeder_abc import DataSeederABC + from cpl.auth.permission.permissions_registry import PermissionsRegistry + from cpl.auth.permission.permissions import Permissions + + try: + collection.add_singleton(DataSeederABC, PermissionSeeder) + PermissionsRegistry.with_enum(Permissions) + except ImportError as e: + from cpl.core.console import Console + + Console.error("cpl-auth is not installed", str(e)) + + +_ServiceCollection.with_module(add_auth, __name__) +_ServiceCollection.with_module(add_permission, _permission.__name__) diff --git a/src/cpl-auth/cpl/auth/auth_logger.py b/src/cpl-auth/cpl/auth/auth_logger.py new file mode 100644 index 00000000..e3b40acb --- /dev/null +++ b/src/cpl-auth/cpl/auth/auth_logger.py @@ -0,0 +1,8 @@ +from cpl.core.log import Logger +from cpl.core.typing import Source + + +class AuthLogger(Logger): + + def __init__(self, source: Source): + Logger.__init__(self, source, "auth") diff --git a/src/cpl-auth/cpl/auth/keycloak/__init__.py b/src/cpl-auth/cpl/auth/keycloak/__init__.py new file mode 100644 index 00000000..caac755d --- /dev/null +++ b/src/cpl-auth/cpl/auth/keycloak/__init__.py @@ -0,0 +1,3 @@ +from .keycloak_admin import KeycloakAdmin +from .keycloak_client import KeycloakClient +from .keycloak_user import KeycloakUser diff --git a/src/cpl-auth/cpl/auth/keycloak/keycloak_admin.py b/src/cpl-auth/cpl/auth/keycloak/keycloak_admin.py new file mode 100644 index 00000000..f2a3ef74 --- /dev/null +++ b/src/cpl-auth/cpl/auth/keycloak/keycloak_admin.py @@ -0,0 +1,24 @@ +from keycloak import KeycloakAdmin as _KeycloakAdmin, KeycloakOpenIDConnection + +from cpl.auth.auth_logger import AuthLogger +from cpl.auth.keycloak_settings import KeycloakSettings + +_logger = AuthLogger("keycloak") + + +class KeycloakAdmin(_KeycloakAdmin): + + def __init__(self, settings: KeycloakSettings): + _logger.info("Initializing Keycloak admin") + _connection = KeycloakOpenIDConnection( + server_url=settings.url, + client_id=settings.client_id, + realm_name=settings.realm, + client_secret_key=settings.client_secret, + ) + _KeycloakAdmin.__init__( + self, + connection=_connection, + ) + + self.__connection = _connection diff --git a/src/cpl-auth/cpl/auth/keycloak/keycloak_client.py b/src/cpl-auth/cpl/auth/keycloak/keycloak_client.py new file mode 100644 index 00000000..df5cd472 --- /dev/null +++ b/src/cpl-auth/cpl/auth/keycloak/keycloak_client.py @@ -0,0 +1,26 @@ +from keycloak import KeycloakOpenID, KeycloakAdmin, KeycloakOpenIDConnection + +from cpl.auth.auth_logger import AuthLogger +from cpl.auth.keycloak_settings import KeycloakSettings + +_logger = AuthLogger("keycloak") + + +class KeycloakClient(KeycloakOpenID): + + def __init__(self, settings: KeycloakSettings): + KeycloakOpenID.__init__( + self, + server_url=settings.url, + client_id=settings.client_id, + realm_name=settings.realm, + client_secret_key=settings.client_secret, + ) + _logger.info("Initializing Keycloak client") + connection = KeycloakOpenIDConnection( + server_url=settings.url, + client_id=settings.client_id, + realm_name=settings.realm, + client_secret_key=settings.client_secret, + ) + self._admin = KeycloakAdmin(connection=connection) diff --git a/src/cpl-auth/cpl/auth/keycloak/keycloak_user.py b/src/cpl-auth/cpl/auth/keycloak/keycloak_user.py new file mode 100644 index 00000000..53bab61d --- /dev/null +++ b/src/cpl-auth/cpl/auth/keycloak/keycloak_user.py @@ -0,0 +1,36 @@ +from cpl.core.utils.get_value import get_value +from cpl.dependency import ServiceProviderABC + + +class KeycloakUser: + + def __init__(self, source: dict): + self._username = get_value(source, "preferred_username", str) + self._email = get_value(source, "email", str) + self._email_verified = get_value(source, "email_verified", bool) + self._name = get_value(source, "name", str) + + @property + def username(self) -> str: + return self._username + + @property + def email(self) -> str: + return self._email + + @property + def email_verified(self) -> bool: + return self._email_verified + + @property + def name(self) -> str: + return self._name + + # Attrs from keycloak + + @property + def id(self) -> str: + from cpl.auth import KeycloakAdmin + + keycloak_admin: KeycloakAdmin = ServiceProviderABC.get_global_service(KeycloakAdmin) + return keycloak_admin.get_user_id(self._username) diff --git a/src/cpl-auth/cpl/auth/keycloak_settings.py b/src/cpl-auth/cpl/auth/keycloak_settings.py new file mode 100644 index 00000000..c2e713f6 --- /dev/null +++ b/src/cpl-auth/cpl/auth/keycloak_settings.py @@ -0,0 +1,37 @@ +from typing import Optional + +from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl.core.environment import Environment + + +class KeycloakSettings(ConfigurationModelABC): + + def __init__( + self, + url: str = Environment.get("KEYCLOAK_URL", str), + client_id: str = Environment.get("KEYCLOAK_CLIENT_ID", str), + realm: str = Environment.get("KEYCLOAK_REALM", str), + client_secret: str = Environment.get("KEYCLOAK_CLIENT_SECRET", str), + ): + ConfigurationModelABC.__init__(self) + + self._url: Optional[str] = url + self._client_id: Optional[str] = client_id + self._realm: Optional[str] = realm + self._client_secret: Optional[str] = client_secret + + @property + def url(self) -> Optional[str]: + return self._url + + @property + def client_id(self) -> Optional[str]: + return self._client_id + + @property + def realm(self) -> Optional[str]: + return self._realm + + @property + def client_secret(self) -> Optional[str]: + return self._client_secret diff --git a/src/cpl-auth/cpl/auth/permission/__init__.py b/src/cpl-auth/cpl/auth/permission/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-auth/cpl/auth/permission/permissions.py b/src/cpl-auth/cpl/auth/permission/permissions.py new file mode 100644 index 00000000..2eefeb29 --- /dev/null +++ b/src/cpl-auth/cpl/auth/permission/permissions.py @@ -0,0 +1,36 @@ +from enum import Enum + + +class Permissions(Enum): + """ """ + + """ + Administration + """ + # administrator + administrator = "administrator" + + # api keys + api_keys = "api_keys" + api_keys_create = "api_keys.create" + api_keys_update = "api_keys.update" + api_keys_delete = "api_keys.delete" + + # users + users = "users" + users_create = "users.create" + users_update = "users.update" + users_delete = "users.delete" + + # settings + settings = "settings" + settings_update = "settings.update" + + """ + Permissions + """ + # roles + roles = "roles" + roles_create = "roles.create" + roles_update = "roles.update" + roles_delete = "roles.delete" diff --git a/src/cpl-auth/cpl/auth/permission/permissions_registry.py b/src/cpl-auth/cpl/auth/permission/permissions_registry.py new file mode 100644 index 00000000..6e2d8748 --- /dev/null +++ b/src/cpl-auth/cpl/auth/permission/permissions_registry.py @@ -0,0 +1,24 @@ +from enum import Enum +from typing import Type + + +class PermissionsRegistry: + _permissions: dict[str, str] = {} + + @classmethod + def get(cls): + return cls._permissions.keys() + + @classmethod + def descriptions(cls): + return {x: cls._permissions[x] for x in cls._permissions if cls._permissions[x] is not None} + + @classmethod + def set(cls, permission: str, description: str = None): + cls._permissions[permission] = description + + @classmethod + def with_enum(cls, e: Type[Enum]): + perms = [x.value for x in e] + for perm in perms: + cls.set(str(perm)) diff --git a/src/cpl-auth/cpl/auth/permission_seeder.py b/src/cpl-auth/cpl/auth/permission_seeder.py new file mode 100644 index 00000000..9ca4c50c --- /dev/null +++ b/src/cpl-auth/cpl/auth/permission_seeder.py @@ -0,0 +1,120 @@ +from cpl.auth.permission.permissions import Permissions +from cpl.auth.permission.permissions_registry import PermissionsRegistry +from cpl.auth.schema import ( + Permission, + Role, + RolePermission, + ApiKey, + ApiKeyPermission, + PermissionDao, + RoleDao, + RolePermissionDao, + ApiKeyDao, + ApiKeyPermissionDao, +) +from cpl.core.utils.get_value import get_value +from cpl.database.abc.data_seeder_abc import DataSeederABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class PermissionSeeder(DataSeederABC): + def __init__( + self, + permission_dao: PermissionDao, + role_dao: RoleDao, + role_permission_dao: RolePermissionDao, + api_key_dao: ApiKeyDao, + api_key_permission_dao: ApiKeyPermissionDao, + ): + DataSeederABC.__init__(self) + self._permission_dao = permission_dao + self._role_dao = role_dao + self._role_permission_dao = role_permission_dao + self._api_key_dao = api_key_dao + self._api_key_permission_dao = api_key_permission_dao + + async def seed(self): + permissions = await self._permission_dao.get_all() + possible_permissions = [permission for permission in PermissionsRegistry.get()] + + if len(permissions) == len(possible_permissions): + _logger.info("Permissions already existing") + await self._update_missing_descriptions() + return + + to_delete = [] + for permission in permissions: + if permission.name in possible_permissions: + continue + + to_delete.append(permission) + + await self._permission_dao.delete_many(to_delete, hard_delete=True) + + _logger.warning("Permissions incomplete") + permission_names = [permission.name for permission in permissions] + await self._permission_dao.create_many( + [ + Permission( + 0, + permission, + get_value(PermissionsRegistry.descriptions(), permission, str), + ) + for permission in possible_permissions + if permission not in permission_names + ] + ) + await self._update_missing_descriptions() + + await self._add_missing_to_role() + await self._add_missing_to_api_key() + + async def _add_missing_to_role(self): + admin_role = await self._role_dao.find_single_by([{Role.id: 1}, {Role.name: "admin"}]) + if admin_role is None: + return + + admin_permissions = await self._role_permission_dao.get_by_role_id(admin_role.id, with_deleted=True) + to_assign = [ + RolePermission(0, admin_role.id, permission.id) + for permission in await self._permission_dao.get_all() + if permission.id not in [x.permission_id for x in admin_permissions] + ] + await self._role_permission_dao.create_many(to_assign) + + async def _add_missing_to_api_key(self): + admin_api_key = await self._api_key_dao.find_single_by([{ApiKey.id: 1}, {ApiKey.identifier: "admin"}]) + if admin_api_key is None: + return + + admin_permissions = await self._api_key_permission_dao.find_by_api_key_id(admin_api_key.id, with_deleted=True) + to_assign = [ + ApiKeyPermission(0, admin_api_key.id, permission.id) + for permission in await self._permission_dao.get_all() + if permission.id not in [x.permission_id for x in admin_permissions] + ] + await self._api_key_permission_dao.create_many(to_assign) + + async def _update_missing_descriptions(self): + permissions = { + permission.name: permission + for permission in await self._permission_dao.find_by([{Permission.description: None}]) + } + to_update = [] + + if len(permissions) == 0: + return + + for key in PermissionsRegistry.descriptions(): + if key.value not in permissions: + continue + + permissions[key.value].description = PermissionsRegistry.descriptions()[key] + to_update.append(permissions[key.value]) + + if len(to_update) == 0: + return + + await self._permission_dao.update_many(to_update) diff --git a/src/cpl-auth/cpl/auth/schema/__init__.py b/src/cpl-auth/cpl/auth/schema/__init__.py new file mode 100644 index 00000000..cdb4b9d1 --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/__init__.py @@ -0,0 +1,15 @@ +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 ._permission.api_key_permission import ApiKeyPermission +from ._permission.api_key_permission_dao import ApiKeyPermissionDao +from ._permission.permission import Permission +from ._permission.permission_dao import PermissionDao +from ._permission.role import Role +from ._permission.role_dao import RoleDao +from ._permission.role_permission import RolePermission +from ._permission.role_permission_dao import RolePermissionDao +from ._permission.role_user import RoleUser +from ._permission.role_user_dao import RoleUserDao diff --git a/src/cpl-auth/cpl/auth/schema/_administration/__init__.py b/src/cpl-auth/cpl/auth/schema/_administration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-auth/cpl/auth/schema/_administration/api_key.py b/src/cpl-auth/cpl/auth/schema/_administration/api_key.py new file mode 100644 index 00000000..845a3ecb --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_administration/api_key.py @@ -0,0 +1,59 @@ +import secrets +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from cpl.auth.permission.permissions import Permissions +from cpl.core.environment import Environment +from cpl.core.log import Logger +from cpl.core.typing import SerialId, Id +from cpl.database.abc import DbModelABC +from cpl.dependency import ServiceProviderABC + +_logger = Logger(__name__) + + +class ApiKey(DbModelABC): + def __init__( + self, + id: SerialId, + identifier: str, + key: str, + deleted: bool = False, + editor_id: Optional[Id] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._identifier = identifier + self._key = key + + @property + def identifier(self) -> str: + return self._identifier + + @property + def key(self) -> str: + return self._key + + @async_property + async def permissions(self): + from cpl.auth.schema._permission.api_key_permission_dao import ApiKeyPermissionDao + + api_key_permission_dao: ApiKeyPermissionDao = ServiceProviderABC.get_global_service(ApiKeyPermissionDao) + return [await x.permission for x in await api_key_permission_dao.find_by_api_key_id(self.id)] + + async def has_permission(self, permission: Permissions) -> bool: + return permission.value in [x.name for x in await self.permissions] + + def set_new_api_key(self): + self._key = self.new_key() + + @staticmethod + def new_key() -> str: + return f"api_{secrets.token_urlsafe(Environment.get("API_KEY_LENGTH", int, 64))}" + + @classmethod + def new(cls, identifier: str) -> "ApiKey": + return ApiKey(0, identifier, cls.new_key()) diff --git a/src/cpl-auth/cpl/auth/schema/_administration/api_key_dao.py b/src/cpl-auth/cpl/auth/schema/_administration/api_key_dao.py new file mode 100644 index 00000000..9db9cc4c --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_administration/api_key_dao.py @@ -0,0 +1,32 @@ +from typing import Optional + +from cpl.auth.schema._administration.api_key import ApiKey +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class ApiKeyDao(DbModelDaoABC[ApiKey]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, ApiKey, TableManager.get("api_keys")) + + self.attribute(ApiKey.identifier, str) + self.attribute(ApiKey.key, str, "keystring") + + async def get_by_identifier(self, ident: str) -> ApiKey: + result = await self._db.select_map(f"SELECT * FROM {self._table_name} WHERE Identifier = '{ident}'") + return self.to_object(result[0]) + + async def get_by_key(self, key: str) -> ApiKey: + result = await self._db.select_map(f"SELECT * FROM {self._table_name} WHERE Keystring = '{key}'") + return self.to_object(result[0]) + + async def find_by_key(self, key: str) -> Optional[ApiKey]: + result = await self._db.select_map(f"SELECT * FROM {self._table_name} WHERE Keystring = '{key}'") + if not result or len(result) == 0: + return None + + return self.to_object(result[0]) diff --git a/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py b/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py new file mode 100644 index 00000000..0ff2a97c --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py @@ -0,0 +1,89 @@ +import uuid +from datetime import datetime +from typing import Optional + +from async_property import async_property +from keycloak import KeycloakGetError + +from cpl.auth import KeycloakAdmin +from cpl.auth.auth_logger import AuthLogger +from cpl.auth.permission.permissions import Permissions +from cpl.core.typing import SerialId +from cpl.database.abc import DbModelABC +from cpl.dependency import ServiceProviderABC + +_logger = AuthLogger(__name__) + + +class AuthUser(DbModelABC): + def __init__( + self, + id: SerialId, + keycloak_id: str, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._keycloak_id = keycloak_id + + @property + def keycloak_id(self) -> str: + return self._keycloak_id + + @property + def username(self): + if self._keycloak_id == str(uuid.UUID(int=0)): + return "ANONYMOUS" + + try: + keycloak_admin: KeycloakAdmin = ServiceProviderABC.get_global_service(KeycloakAdmin) + return keycloak_admin.get_user(self._keycloak_id).get("username") + except KeycloakGetError as e: + return "UNKNOWN" + except Exception as e: + _logger.error(f"Failed to get user {self._keycloak_id} from Keycloak", e) + return "UNKNOWN" + + @property + def email(self): + if self._keycloak_id == str(uuid.UUID(int=0)): + return "ANONYMOUS" + + try: + keycloak_admin: KeycloakAdmin = ServiceProviderABC.get_global_service(KeycloakAdmin) + return keycloak_admin.get_user(self._keycloak_id).get("email") + except KeycloakGetError as e: + return "UNKNOWN" + except Exception as e: + _logger.error(f"Failed to get user {self._keycloak_id} from Keycloak", e) + return "UNKNOWN" + + @async_property + async def roles(self): + from cpl.auth.schema._permission.role_user_dao import RoleUserDao + + role_user_dao: RoleUserDao = ServiceProviderABC.get_global_service(RoleUserDao) + return [await x.role for x in await role_user_dao.get_by_user_id(self.id)] + + @async_property + async def permissions(self): + from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + + auth_user_dao: AuthUserDao = ServiceProviderABC.get_global_service(AuthUserDao) + return await auth_user_dao.get_permissions(self.id) + + async def has_permission(self, permission: Permissions) -> bool: + from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + + auth_user_dao: AuthUserDao = ServiceProviderABC.get_global_service(AuthUserDao) + return await auth_user_dao.has_permission(self.id, permission) + + async def anonymize(self): + from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + + auth_user_dao: AuthUserDao = ServiceProviderABC.get_global_service(AuthUserDao) + + self._keycloak_id = str(uuid.UUID(int=0)) + await auth_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/auth_user_dao.py new file mode 100644 index 00000000..e9d8f478 --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_administration/auth_user_dao.py @@ -0,0 +1,72 @@ +from typing import Optional, Union + +from cpl.auth.permission.permissions import Permissions +from cpl.auth.schema._administration.auth_user import AuthUser +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger +from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder +from cpl.dependency import ServiceProviderABC + +_logger = DBLogger(__name__) + + +class AuthUserDao(DbModelDaoABC[AuthUser]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, AuthUser, TableManager.get("auth_users")) + + self.attribute(AuthUser.keycloak_id, str, aliases=["keycloakId"]) + + async def get_users(): + return [(x.id, x.username, x.email) for x in await self.get_all()] + + self.use_external_fields( + ExternalDataTempTableBuilder() + .with_table_name(self._table_name) + .with_field("id", "int", True) + .with_field("username", "text") + .with_field("email", "text") + .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 find_by_keycloak_id(self, keycloak_id: str) -> Optional[AuthUser]: + return await self.find_single_by({AuthUser.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 + + permission_dao: PermissionDao = ServiceProviderABC.get_global_service(PermissionDao) + p = await permission_dao.get_by_name(permission if isinstance(permission, str) else permission.value) + result = await self._db.select_map( + f""" + SELECT COUNT(*) + FROM permission.role_users ru + JOIN permission.role_permissions rp ON ru.roleId = rp.roleId + WHERE ru.userId = {user_id} + AND rp.permissionId = {p.id} + AND ru.deleted = FALSE + AND rp.deleted = FALSE; + """ + ) + if result is None or len(result) == 0: + return False + + return result[0]["count"] > 0 + + async def get_permissions(self, user_id: int) -> list[Permissions]: + result = await self._db.select_map( + f""" + SELECT p.* + FROM permission.permissions p + JOIN permission.role_permissions rp ON p.id = rp.permissionId + JOIN permission.role_users ru ON rp.roleId = ru.roleId + WHERE ru.userId = {user_id} + AND rp.deleted = FALSE + AND ru.deleted = FALSE; + """ + ) + return [Permissions(p["name"]) for p in result] diff --git a/src/cpl-auth/cpl/auth/schema/_permission/__init__.py b/src/cpl-auth/cpl/auth/schema/_permission/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-auth/cpl/auth/schema/_permission/api_key_permission.py b/src/cpl-auth/cpl/auth/schema/_permission/api_key_permission.py new file mode 100644 index 00000000..a3751b0f --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/api_key_permission.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from cpl.core.typing import SerialId +from cpl.database.abc import DbJoinModelABC +from cpl.dependency import ServiceProviderABC + + +class ApiKeyPermission(DbJoinModelABC): + def __init__( + self, + id: SerialId, + api_key_id: SerialId, + permission_id: SerialId, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbJoinModelABC.__init__(self, api_key_id, permission_id, id, deleted, editor_id, created, updated) + self._api_key_id = api_key_id + self._permission_id = permission_id + + @property + def api_key_id(self) -> int: + return self._api_key_id + + @async_property + async def api_key(self): + from cpl.auth.schema._administration.api_key_dao import ApiKeyDao + + api_key_dao: ApiKeyDao = ServiceProviderABC.get_global_service(ApiKeyDao) + return await api_key_dao.get_by_id(self._api_key_id) + + @property + def permission_id(self) -> int: + return self._permission_id + + @async_property + async def permission(self): + from cpl.auth.schema._permission.permission_dao import PermissionDao + + permission_dao: PermissionDao = ServiceProviderABC.get_global_service(PermissionDao) + return await permission_dao.get_by_id(self._permission_id) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/api_key_permission_dao.py b/src/cpl-auth/cpl/auth/schema/_permission/api_key_permission_dao.py new file mode 100644 index 00000000..935928bd --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/api_key_permission_dao.py @@ -0,0 +1,29 @@ +from cpl.auth.schema._permission.api_key_permission import ApiKeyPermission +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class ApiKeyPermissionDao(DbModelDaoABC[ApiKeyPermission]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, ApiKeyPermission, TableManager.get("api_key_permissions")) + + self.attribute(ApiKeyPermission.api_key_id, int) + self.attribute(ApiKeyPermission.permission_id, int) + + async def find_by_api_key_id(self, api_key_id: int, with_deleted=False) -> list[ApiKeyPermission]: + f = [{ApiKeyPermission.api_key_id: api_key_id}] + if not with_deleted: + f.append({ApiKeyPermission.deleted: False}) + + return await self.find_by(f) + + async def find_by_permission_id(self, permission_id: int, with_deleted=False) -> list[ApiKeyPermission]: + f = [{ApiKeyPermission.permission_id: permission_id}] + if not with_deleted: + f.append({ApiKeyPermission.deleted: False}) + + return await self.find_by(f) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/permission.py b/src/cpl-auth/cpl/auth/schema/_permission/permission.py new file mode 100644 index 00000000..e5bb046d --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/permission.py @@ -0,0 +1,37 @@ +from datetime import datetime +from typing import Optional + +from cpl.core.typing import SerialId +from cpl.database.abc import DbModelABC + + +class Permission(DbModelABC): + def __init__( + self, + id: SerialId, + name: str, + description: str, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._name = name + self._description = description + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str): + self._description = value diff --git a/src/cpl-auth/cpl/auth/schema/_permission/permission_dao.py b/src/cpl-auth/cpl/auth/schema/_permission/permission_dao.py new file mode 100644 index 00000000..c75fd7bb --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/permission_dao.py @@ -0,0 +1,21 @@ +from typing import Optional + +from cpl.auth.schema._permission.permission import Permission +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class PermissionDao(DbModelDaoABC[Permission]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, Permission, TableManager.get("permissions")) + + self.attribute(Permission.name, str) + self.attribute(Permission.description, Optional[str]) + + async def get_by_name(self, name: str) -> Permission: + result = await self._db.select_map(f"SELECT * FROM {self._table_name} WHERE Name = '{name}'") + return self.to_object(result[0]) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role.py b/src/cpl-auth/cpl/auth/schema/_permission/role.py new file mode 100644 index 00000000..9e0a4b72 --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/role.py @@ -0,0 +1,66 @@ +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from cpl.auth.permission.permissions import Permissions +from cpl.core.typing import SerialId +from cpl.database.abc import DbModelABC +from cpl.dependency import ServiceProviderABC + + +class Role(DbModelABC): + def __init__( + self, + id: SerialId, + name: str, + description: str, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._name = name + self._description = description + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str): + self._description = value + + @async_property + async def permissions(self): + from cpl.auth.schema._permission.role_permission_dao import RolePermissionDao + + role_permission_dao: RolePermissionDao = ServiceProviderABC.get_global_service(RolePermissionDao) + return [await x.permission for x in await role_permission_dao.get_by_role_id(self.id)] + + @async_property + async def users(self): + from cpl.auth.schema._permission.role_user_dao import RoleUserDao + + role_user_dao: RoleUserDao = ServiceProviderABC.get_global_service(RoleUserDao) + return [await x.user for x in await role_user_dao.get_by_role_id(self.id)] + + async def has_permission(self, permission: Permissions) -> bool: + from cpl.auth.schema._permission.permission_dao import PermissionDao + from cpl.auth.schema._permission.role_permission_dao import RolePermissionDao + + permission_dao: PermissionDao = ServiceProviderABC.get_global_service(PermissionDao) + role_permission_dao: RolePermissionDao = ServiceProviderABC.get_global_service(RolePermissionDao) + + p = await permission_dao.get_by_name(permission.value) + + return p.id in [x.id for x in await role_permission_dao.get_by_role_id(self.id)] diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role_dao.py b/src/cpl-auth/cpl/auth/schema/_permission/role_dao.py new file mode 100644 index 00000000..be620094 --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/role_dao.py @@ -0,0 +1,17 @@ +from cpl.auth.schema._permission.role import Role +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class RoleDao(DbModelDaoABC[Role]): + def __init__(self): + DbModelDaoABC.__init__(self, __name__, Role, TableManager.get("roles")) + self.attribute(Role.name, str) + self.attribute(Role.description, str) + + async def get_by_name(self, name: str) -> Role: + result = await self._db.select_map(f"SELECT * FROM {self._table_name} WHERE Name = '{name}'") + return self.to_object(result[0]) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role_permission.py b/src/cpl-auth/cpl/auth/schema/_permission/role_permission.py new file mode 100644 index 00000000..4fdcdcbe --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/role_permission.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from cpl.core.typing import SerialId +from cpl.database.abc import DbModelABC +from cpl.dependency import ServiceProviderABC + + +class RolePermission(DbModelABC): + def __init__( + self, + id: SerialId, + role_id: SerialId, + permission_id: SerialId, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._role_id = role_id + self._permission_id = permission_id + + @property + def role_id(self) -> int: + return self._role_id + + @async_property + async def role(self): + from cpl.auth.schema._permission.role_dao import RoleDao + + role_dao: RoleDao = ServiceProviderABC.get_global_service(RoleDao) + return await role_dao.get_by_id(self._role_id) + + @property + def permission_id(self) -> int: + return self._permission_id + + @async_property + async def permission(self): + from cpl.auth.schema._permission.permission_dao import PermissionDao + + permission_dao: PermissionDao = ServiceProviderABC.get_global_service(PermissionDao) + return await permission_dao.get_by_id(self._permission_id) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role_permission_dao.py b/src/cpl-auth/cpl/auth/schema/_permission/role_permission_dao.py new file mode 100644 index 00000000..59f60786 --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/role_permission_dao.py @@ -0,0 +1,29 @@ +from cpl.auth.schema._permission.role_permission import RolePermission +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class RolePermissionDao(DbModelDaoABC[RolePermission]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, RolePermission, TableManager.get("role_permissions")) + + self.attribute(RolePermission.role_id, int) + self.attribute(RolePermission.permission_id, int) + + async def get_by_role_id(self, role_id: int, with_deleted=False) -> list[RolePermission]: + f = [{RolePermission.role_id: role_id}] + if not with_deleted: + f.append({RolePermission.deleted: False}) + + return await self.find_by(f) + + async def get_by_permission_id(self, permission_id: int, with_deleted=False) -> list[RolePermission]: + f = [{RolePermission.permission_id: permission_id}] + if not with_deleted: + f.append({RolePermission.deleted: False}) + + return await self.find_by(f) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role_user.py b/src/cpl-auth/cpl/auth/schema/_permission/role_user.py new file mode 100644 index 00000000..a4ed13fc --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/role_user.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from cpl.core.typing import SerialId +from cpl.database.abc import DbJoinModelABC +from cpl.dependency import ServiceProviderABC + + +class RoleUser(DbJoinModelABC): + def __init__( + self, + id: SerialId, + user_id: SerialId, + role_id: SerialId, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbJoinModelABC.__init__(self, id, user_id, role_id, deleted, editor_id, created, updated) + self._user_id = user_id + self._role_id = role_id + + @property + def user_id(self) -> int: + return self._user_id + + @async_property + async def user(self): + from cpl.auth.schema._administration.auth_user_dao import AuthUserDao + + auth_user_dao: AuthUserDao = ServiceProviderABC.get_global_service(AuthUserDao) + return await auth_user_dao.get_by_id(self._user_id) + + @property + def role_id(self) -> int: + return self._role_id + + @async_property + async def role(self): + from cpl.auth.schema._permission.role_dao import RoleDao + + role_dao: RoleDao = ServiceProviderABC.get_global_service(RoleDao) + return await role_dao.get_by_id(self._role_id) diff --git a/src/cpl-auth/cpl/auth/schema/_permission/role_user_dao.py b/src/cpl-auth/cpl/auth/schema/_permission/role_user_dao.py new file mode 100644 index 00000000..90a0a888 --- /dev/null +++ b/src/cpl-auth/cpl/auth/schema/_permission/role_user_dao.py @@ -0,0 +1,29 @@ +from cpl.auth.schema._permission.role_user import RoleUser +from cpl.database import TableManager +from cpl.database.abc import DbModelDaoABC +from cpl.database.db_logger import DBLogger + +_logger = DBLogger(__name__) + + +class RoleUserDao(DbModelDaoABC[RoleUser]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, RoleUser, TableManager.get("role_users")) + + self.attribute(RoleUser.role_id, int) + self.attribute(RoleUser.user_id, int) + + async def get_by_role_id(self, rid: int, with_deleted=False) -> list[RoleUser]: + f = [{RoleUser.role_id: rid}] + if not with_deleted: + f.append({RoleUser.deleted: False}) + + return await self.find_by(f) + + async def get_by_user_id(self, uid: int, with_deleted=False) -> list[RoleUser]: + f = [{RoleUser.user_id: uid}] + if not with_deleted: + f.append({RoleUser.deleted: False}) + + return await self.find_by(f) diff --git a/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql b/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql new file mode 100644 index 00000000..59bb4f66 --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/mysql/1-users.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS administration_auth_users +( + id INT AUTO_INCREMENT PRIMARY KEY, + keycloakId CHAR(36) NOT NULL, + -- for history + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + 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) +); + +CREATE TABLE IF NOT EXISTS administration_auth_users_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + keycloakId CHAR(36) NOT NULL, + -- for history + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + +CREATE TRIGGER TR_administration_auth_usersUpdate + AFTER UPDATE + ON administration_auth_users + FOR EACH ROW +BEGIN + INSERT INTO administration_auth_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 + AFTER DELETE + ON administration_auth_users + FOR EACH ROW +BEGIN + INSERT INTO administration_auth_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 new file mode 100644 index 00000000..35f76066 --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/mysql/2-api-key.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS administration_api_keys +( + id INT AUTO_INCREMENT PRIMARY KEY, + identifier VARCHAR(255) NOT NULL, + keyString VARCHAR(255) NOT NULL, + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + CONSTRAINT UC_Identifier_Key UNIQUE (identifier, keyString), + CONSTRAINT UC_Key UNIQUE (keyString), + CONSTRAINT FK_ApiKeys_Editor FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) +); + +CREATE TABLE IF NOT EXISTS administration_api_keys_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + identifier VARCHAR(255) NOT NULL, + keyString VARCHAR(255) NOT NULL, + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + + +CREATE TRIGGER TR_ApiKeysUpdate + AFTER UPDATE + ON administration_api_keys + FOR EACH ROW +BEGIN + INSERT INTO administration_api_keys_history + (id, identifier, keyString, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.identifier, OLD.keyString, OLD.deleted, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TRIGGER TR_ApiKeysDelete + AFTER DELETE + ON administration_api_keys + FOR EACH ROW +BEGIN + INSERT INTO administration_api_keys_history + (id, identifier, keyString, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.identifier, OLD.keyString, 1, OLD.editorId, OLD.created, NOW()); +END; 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 new file mode 100644 index 00000000..afb20166 --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/mysql/3-roles-permissions.sql @@ -0,0 +1,179 @@ +CREATE TABLE IF NOT EXISTS permission_permissions +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NULL, + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + 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) +); + +CREATE TABLE IF NOT EXISTS permission_permissions_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NULL, + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + +CREATE TRIGGER TR_PermissionsUpdate + AFTER UPDATE + ON permission_permissions + FOR EACH ROW +BEGIN + INSERT INTO permission_permissions_history + (id, name, description, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.name, OLD.description, OLD.deleted, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TRIGGER TR_PermissionsDelete + AFTER DELETE + ON permission_permissions + FOR EACH ROW +BEGIN + INSERT INTO permission_permissions_history + (id, name, description, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.name, OLD.description, 1, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TABLE IF NOT EXISTS permission_roles +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NULL, + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + 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) +); + +CREATE TABLE IF NOT EXISTS permission_roles_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NULL, + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + +CREATE TRIGGER TR_RolesUpdate + AFTER UPDATE + ON permission_roles + FOR EACH ROW +BEGIN + INSERT INTO permission_roles_history + (id, name, description, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.name, OLD.description, OLD.deleted, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TRIGGER TR_RolesDelete + AFTER DELETE + ON permission_roles + FOR EACH ROW +BEGIN + INSERT INTO permission_roles_history + (id, name, description, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.name, OLD.description, 1, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TABLE IF NOT EXISTS permission_role_permissions +( + id INT AUTO_INCREMENT PRIMARY KEY, + RoleId INT NOT NULL, + permissionId INT NOT NULL, + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + 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) +); + +CREATE TABLE IF NOT EXISTS permission_role_permissions_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + RoleId INT NOT NULL, + permissionId INT NOT NULL, + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + +CREATE TRIGGER TR_RolePermissionsUpdate + AFTER UPDATE + ON permission_role_permissions + FOR EACH ROW +BEGIN + INSERT INTO permission_role_permissions_history + (id, RoleId, permissionId, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.RoleId, OLD.permissionId, OLD.deleted, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TRIGGER TR_RolePermissionsDelete + AFTER DELETE + ON permission_role_permissions + FOR EACH ROW +BEGIN + INSERT INTO permission_role_permissions_history + (id, RoleId, permissionId, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.RoleId, OLD.permissionId, 1, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TABLE IF NOT EXISTS permission_role_auth_users +( + id INT AUTO_INCREMENT PRIMARY KEY, + RoleId INT NOT NULL, + UserId INT NOT NULL, + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + 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) +); + +CREATE TABLE IF NOT EXISTS permission_role_auth_users_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + RoleId INT NOT NULL, + UserId INT NOT NULL, + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + +CREATE TRIGGER TR_Roleauth_usersUpdate + AFTER UPDATE + ON permission_role_auth_users + FOR EACH ROW +BEGIN + INSERT INTO permission_role_auth_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 + AFTER DELETE + ON permission_role_auth_users + FOR EACH ROW +BEGIN + INSERT INTO permission_role_auth_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 new file mode 100644 index 00000000..2770838f --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/mysql/4-api-key-permissions.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS permission_api_key_permissions +( + id INT AUTO_INCREMENT PRIMARY KEY, + apiKeyId INT NOT NULL, + permissionId INT NOT NULL, + deleted BOOL NOT NULL DEFAULT FALSE, + editorId INT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + 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) +); + +CREATE TABLE IF NOT EXISTS permission_api_key_permissions_history +( + id INT AUTO_INCREMENT PRIMARY KEY, + apiKeyId INT NOT NULL, + permissionId INT NOT NULL, + deleted BOOL NOT NULL, + editorId INT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL +); + +CREATE TRIGGER TR_ApiKeyPermissionsUpdate + AFTER UPDATE + ON permission_api_key_permissions + FOR EACH ROW +BEGIN + INSERT INTO permission_api_key_permissions_history + (id, apiKeyId, permissionId, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.apiKeyId, OLD.permissionId, OLD.deleted, OLD.editorId, OLD.created, NOW()); +END; + +CREATE TRIGGER TR_ApiKeyPermissionsDelete + AFTER DELETE + ON permission_api_key_permissions + FOR EACH ROW +BEGIN + INSERT INTO permission_api_key_permissions_history + (id, apiKeyId, permissionId, deleted, editorId, created, updated) + VALUES (OLD.id, OLD.apiKeyId, OLD.permissionId, 1, OLD.editorId, OLD.created, NOW()); +END; + diff --git a/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql b/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql new file mode 100644 index 00000000..41d15483 --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/postgres/1-users.sql @@ -0,0 +1,26 @@ +CREATE SCHEMA IF NOT EXISTS administration; + +CREATE TABLE IF NOT EXISTS administration.auth_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), + 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 +( + LIKE administration.auth_users +); + +CREATE TRIGGER users_history_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON administration.auth_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 new file mode 100644 index 00000000..9944d667 --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/postgres/2-api-key.sql @@ -0,0 +1,28 @@ +CREATE SCHEMA IF NOT EXISTS administration; + +CREATE TABLE IF NOT EXISTS administration.api_keys +( + id SERIAL PRIMARY KEY, + identifier VARCHAR(255) NOT NULL, + keyString VARCHAR(255) NOT NULL, + -- for history + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL REFERENCES administration.auth_users (id), + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW(), + + CONSTRAINT UC_Identifier_Key UNIQUE (identifier, keyString), + CONSTRAINT UC_Key UNIQUE (keyString) +); + +CREATE TABLE IF NOT EXISTS administration.api_keys_history +( + LIKE administration.api_keys +); + +CREATE TRIGGER api_keys_history_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON administration.api_keys + FOR EACH ROW +EXECUTE FUNCTION public.history_trigger_function(); + 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 new file mode 100644 index 00000000..42b9283b --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/postgres/3-roles-permissions.sql @@ -0,0 +1,105 @@ +CREATE SCHEMA IF NOT EXISTS permission; + +-- Permissions +CREATE TABLE permission.permissions +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NULL, + + -- for history + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL REFERENCES administration.auth_users (id), + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_PermissionName UNIQUE (name) +); + +CREATE TABLE permission.permissions_history +( + LIKE permission.permissions +); + +CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON permission.permissions + FOR EACH ROW +EXECUTE PROCEDURE public.history_trigger_function(); + +-- Roles +CREATE TABLE permission.roles +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NULL, + + -- for history + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL REFERENCES administration.auth_users (id), + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_RoleName UNIQUE (name) +); + +CREATE TABLE permission.roles_history +( + LIKE permission.roles +); + +CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON permission.roles + FOR EACH ROW +EXECUTE PROCEDURE public.history_trigger_function(); + +-- Role permissions +CREATE TABLE permission.role_permissions +( + id SERIAL PRIMARY KEY, + RoleId INT NOT NULL REFERENCES permission.roles (id) ON DELETE CASCADE, + permissionId INT NOT NULL REFERENCES permission.permissions (id) ON DELETE CASCADE, + + -- for history + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL REFERENCES administration.auth_users (id), + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_RolePermission UNIQUE (RoleId, permissionId) +); + +CREATE TABLE permission.role_permissions_history +( + LIKE permission.role_permissions +); + +CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON permission.role_permissions + FOR EACH ROW +EXECUTE PROCEDURE public.history_trigger_function(); + +-- Role user +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, + + -- for history + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL REFERENCES administration.auth_users (id), + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_RoleUser UNIQUE (RoleId, UserId) +); + +CREATE TABLE permission.role_users_history +( + LIKE permission.role_users +); + +CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE + 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 new file mode 100644 index 00000000..18e0d706 --- /dev/null +++ b/src/cpl-auth/cpl/auth/scripts/postgres/4-api-key-permissions.sql @@ -0,0 +1,24 @@ +CREATE TABLE permission.api_key_permissions +( + id SERIAL PRIMARY KEY, + apiKeyId INT NOT NULL REFERENCES administration.api_keys (id) ON DELETE CASCADE, + permissionId INT NOT NULL REFERENCES permission.permissions (id) ON DELETE CASCADE, + + -- for history + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL REFERENCES administration.auth_users (id), + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_ApiKeyPermission UNIQUE (apiKeyId, permissionId) +); + +CREATE TABLE permission.api_key_permissions_history +( + LIKE permission.api_key_permissions +); + +CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON permission.api_key_permissions + FOR EACH ROW +EXECUTE PROCEDURE public.history_trigger_function(); \ No newline at end of file diff --git a/src/cpl-auth/pyproject.toml b/src/cpl-auth/pyproject.toml new file mode 100644 index 00000000..b5f6b008 --- /dev/null +++ b/src/cpl-auth/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["setuptools>=70.1.0", "wheel>=0.43.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "cpl-auth" +version = "2024.7.0" +description = "CPL auth" +readme ="CPL auth package" +requires-python = ">=3.12" +license = { text = "MIT" } +authors = [ + { name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" } +] +keywords = ["cpl", "auth", "backend", "shared", "library"] + +dynamic = ["dependencies", "optional-dependencies"] + +[project.urls] +Homepage = "https://www.sh-edraft.de" + +[tool.setuptools.packages.find] +where = ["."] +include = ["cpl*"] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } +optional-dependencies.dev = { file = ["requirements.dev.txt"] } + + diff --git a/src/cpl-auth/requirements.dev.txt b/src/cpl-auth/requirements.dev.txt new file mode 100644 index 00000000..e7664b42 --- /dev/null +++ b/src/cpl-auth/requirements.dev.txt @@ -0,0 +1 @@ +black==25.1.0 \ No newline at end of file diff --git a/src/cpl-auth/requirements.txt b/src/cpl-auth/requirements.txt new file mode 100644 index 00000000..9ea16469 --- /dev/null +++ b/src/cpl-auth/requirements.txt @@ -0,0 +1,4 @@ +cpl-core +cpl-dependency +cpl-database +python-keycloak-5.8.1 \ No newline at end of file diff --git a/src/cpl-core/cpl/core/__init__.py b/src/cpl-core/cpl/core/__init__.py index 8b137891..e69de29b 100644 --- a/src/cpl-core/cpl/core/__init__.py +++ b/src/cpl-core/cpl/core/__init__.py @@ -1 +0,0 @@ - diff --git a/src/cpl-core/cpl/core/ctx/__init__.py b/src/cpl-core/cpl/core/ctx/__init__.py new file mode 100644 index 00000000..6c973036 --- /dev/null +++ b/src/cpl-core/cpl/core/ctx/__init__.py @@ -0,0 +1 @@ +from .user_context import set_user, get_user diff --git a/src/cpl-core/cpl/core/ctx/user_context.py b/src/cpl-core/cpl/core/ctx/user_context.py new file mode 100644 index 00000000..5a4b6eae --- /dev/null +++ b/src/cpl-core/cpl/core/ctx/user_context.py @@ -0,0 +1,18 @@ +from contextvars import ContextVar +from typing import Optional + +from cpl.auth.auth_logger import AuthLogger +from cpl.auth.schema._administration.auth_user import AuthUser + +_user_context: ContextVar[Optional[AuthUser]] = ContextVar("user", default=None) + +_logger = AuthLogger(__name__) + + +def set_user(user_id: Optional[AuthUser]): + _logger.trace("Setting user context", user_id) + _user_context.set(user_id) + + +def get_user() -> Optional[AuthUser]: + return _user_context.get() diff --git a/src/cpl-database/cpl/database/__init__.py b/src/cpl-database/cpl/database/__init__.py index a982d815..401116be 100644 --- a/src/cpl-database/cpl/database/__init__.py +++ b/src/cpl-database/cpl/database/__init__.py @@ -3,16 +3,17 @@ from typing import Type from cpl.dependency import ServiceCollection as _ServiceCollection from . import mysql as _mysql from . import postgres as _postgres -from .internal_tables import InternalTables +from .table_manager import TableManager -def _add(collection: _ServiceCollection,db_context: Type, default_port: int, server_type: str): +def _add(collection: _ServiceCollection, db_context: Type, default_port: int, server_type: str): from cpl.core.console import Console from cpl.core.configuration import Configuration from cpl.database.abc.db_context_abc import DBContextABC from cpl.database.model.server_type import ServerTypes, ServerType from cpl.database.model.database_settings import DatabaseSettings from cpl.database.service.migration_service import MigrationService + from cpl.database.service.seeder_service import SeederService from cpl.database.schema.executed_migration_dao import ExecutedMigrationDao try: @@ -22,20 +23,25 @@ def _add(collection: _ServiceCollection,db_context: Type, default_port: int, ser collection.add_singleton(DBContextABC, db_context) collection.add_singleton(ExecutedMigrationDao) collection.add_singleton(MigrationService) + collection.add_singleton(SeederService) except ImportError as e: Console.error("cpl-database is not installed", str(e)) + def add_mysql(collection: _ServiceCollection): from cpl.database.mysql.db_context import DBContext from cpl.database.model import ServerTypes + _add(collection, DBContext, 3306, ServerTypes.MYSQL.value) def add_postgres(collection: _ServiceCollection): from cpl.database.mysql.db_context import DBContext from cpl.database.model import ServerTypes + _add(collection, DBContext, 5432, ServerTypes.POSTGRES.value) + _ServiceCollection.with_module(add_mysql, _mysql.__name__) _ServiceCollection.with_module(add_postgres, _postgres.__name__) diff --git a/src/cpl-database/cpl/database/abc/data_access_object_abc.py b/src/cpl-database/cpl/database/abc/data_access_object_abc.py index ed7ebad0..5440cc8f 100644 --- a/src/cpl-database/cpl/database/abc/data_access_object_abc.py +++ b/src/cpl-database/cpl/database/abc/data_access_object_abc.py @@ -4,13 +4,14 @@ from enum import Enum from types import NoneType from typing import Generic, Optional, Union, Type, List, Any +from cpl.core.ctx import get_user from cpl.core.typing import T, Id from cpl.core.utils import String from cpl.core.utils.get_value import get_value -from cpl.database._external_data_temp_table_builder import ExternalDataTempTableBuilder from cpl.database.abc.db_context_abc import DBContextABC from cpl.database.const import DATETIME_FORMAT from cpl.database.db_logger import DBLogger +from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder from cpl.database.postgres.sql_select_builder import SQLSelectBuilder from cpl.database.typing import T_DBM, Attribute, AttributeFilters, AttributeSorts @@ -21,7 +22,7 @@ class DataAccessObjectABC(ABC, Generic[T_DBM]): def __init__(self, source: str, model_type: Type[T_DBM], table_name: str): from cpl.dependency.service_provider_abc import ServiceProviderABC - self._db = ServiceProviderABC.get_global_provider().get_service(DBContextABC) + self._db = ServiceProviderABC.get_global_service(DBContextABC) self._logger = DBLogger(source) self._model_type = model_type @@ -867,9 +868,9 @@ class DataAccessObjectABC(ABC, Generic[T_DBM]): @staticmethod async def _get_editor_id(obj: T_DBM): editor_id = obj.editor_id - # if editor_id is None: - # user = get_user() - # if user is not None: - # editor_id = user.id + if editor_id is None: + user = get_user() + if user is not None: + editor_id = user.id return editor_id if editor_id is not None else "NULL" diff --git a/src/cpl-database/cpl/database/abc/data_seeder_abc.py b/src/cpl-database/cpl/database/abc/data_seeder_abc.py new file mode 100644 index 00000000..3d9d9ecd --- /dev/null +++ b/src/cpl-database/cpl/database/abc/data_seeder_abc.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class DataSeederABC(ABC): + + @abstractmethod + async def seed(self): + pass 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 760ece86..f53e6658 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 @@ -2,9 +2,9 @@ from abc import abstractmethod from datetime import datetime from typing import Type +from cpl.database import TableManager from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.abc.db_model_abc import DbModelABC -from cpl.database.internal_tables import InternalTables class DbModelDaoABC[T_DBM](DataAccessObjectABC[T_DBM]): @@ -15,10 +15,10 @@ class DbModelDaoABC[T_DBM](DataAccessObjectABC[T_DBM]): self.attribute(DbModelABC.id, int, ignore=True) self.attribute(DbModelABC.deleted, bool) - self.attribute(DbModelABC.editor_id, int, ignore=True) # handled by db trigger + self.attribute(DbModelABC.editor_id, int, db_name="editorId", ignore=True) # handled by db trigger self.reference( - "editor", "id", DbModelABC.editor_id, InternalTables.users + "editor", "id", DbModelABC.editor_id, TableManager.get("auth_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/abc/table_abc.py b/src/cpl-database/cpl/database/abc/table_abc.py deleted file mode 100644 index 748503bd..00000000 --- a/src/cpl-database/cpl/database/abc/table_abc.py +++ /dev/null @@ -1,37 +0,0 @@ -from abc import ABC, abstractmethod -from datetime import datetime -from typing import Optional - - -class TableABC(ABC): - @abstractmethod - def __init__(self): - self._created_at: Optional[datetime] = datetime.now().isoformat() - self._modified_at: Optional[datetime] = datetime.now().isoformat() - - @property - def created_at(self) -> datetime: - return self._created_at - - @property - def modified_at(self) -> datetime: - return self._modified_at - - @modified_at.setter - def modified_at(self, value: datetime): - self._modified_at = value - - @property - @abstractmethod - def insert_string(self) -> str: - pass - - @property - @abstractmethod - def udpate_string(self) -> str: - pass - - @property - @abstractmethod - def delete_string(self) -> str: - pass diff --git a/src/cpl-database/cpl/database/_external_data_temp_table_builder.py b/src/cpl-database/cpl/database/external_data_temp_table_builder.py similarity index 100% rename from src/cpl-database/cpl/database/_external_data_temp_table_builder.py rename to src/cpl-database/cpl/database/external_data_temp_table_builder.py diff --git a/src/cpl-database/cpl/database/internal_tables.py b/src/cpl-database/cpl/database/internal_tables.py deleted file mode 100644 index 07d7e667..00000000 --- a/src/cpl-database/cpl/database/internal_tables.py +++ /dev/null @@ -1,15 +0,0 @@ -from cpl.database.model.server_type import ServerTypes, ServerType - - - -class InternalTables: - - @classmethod - @property - def users(cls) -> str: - return "administration.users" if ServerType.server_type is ServerTypes.POSTGRES else "users" - - @classmethod - @property - def executed_migrations(cls) -> str: - return "system._executed_migrations" if ServerType.server_type is ServerTypes.POSTGRES else "_executed_migrations" diff --git a/src/cpl-database/cpl/database/model/server_type.py b/src/cpl-database/cpl/database/model/server_type.py index dbdd40e0..54dfa123 100644 --- a/src/cpl-database/cpl/database/model/server_type.py +++ b/src/cpl-database/cpl/database/model/server_type.py @@ -5,6 +5,7 @@ class ServerTypes(Enum): POSTGRES = "postgres" MYSQL = "mysql" + class ServerType: _server_type: ServerTypes = None @@ -18,4 +19,4 @@ class ServerType: @property def server_type(cls) -> ServerTypes: assert cls._server_type is not None, "Server type is not set" - return cls._server_type \ No newline at end of file + return cls._server_type diff --git a/src/cpl-database/cpl/database/mysql/mysql_pool.py b/src/cpl-database/cpl/database/mysql/mysql_pool.py index 9faed3ce..225aaca1 100644 --- a/src/cpl-database/cpl/database/mysql/mysql_pool.py +++ b/src/cpl-database/cpl/database/mysql/mysql_pool.py @@ -31,7 +31,6 @@ class MySQLPool: db=self._db_settings.database, minsize=1, maxsize=Environment.get("DB_POOL_SIZE", int, 1), - autocommit=True, ) except Exception as e: _logger.fatal("Failed to connect to the database", e) @@ -62,6 +61,7 @@ class MySQLPool: async with pool.acquire() as con: async with con.cursor() as cursor: await self._exec_sql(cursor, query, args, multi) + await con.commit() if cursor.description is not None: # Query returns rows res = await cursor.fetchall() diff --git a/src/cpl-database/cpl/database/postgres/sql_select_builder.py b/src/cpl-database/cpl/database/postgres/sql_select_builder.py index 08487628..23900450 100644 --- a/src/cpl-database/cpl/database/postgres/sql_select_builder.py +++ b/src/cpl-database/cpl/database/postgres/sql_select_builder.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from cpl.database._external_data_temp_table_builder import ExternalDataTempTableBuilder +from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder class SQLSelectBuilder: diff --git a/src/cpl-database/cpl/database/schema/executed_migration_dao.py b/src/cpl-database/cpl/database/schema/executed_migration_dao.py index cef92ce3..89092011 100644 --- a/src/cpl-database/cpl/database/schema/executed_migration_dao.py +++ b/src/cpl-database/cpl/database/schema/executed_migration_dao.py @@ -1,4 +1,4 @@ -from cpl.database import InternalTables +from cpl.database import TableManager from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.db_logger import DBLogger from cpl.database.schema.executed_migration import ExecutedMigration @@ -9,6 +9,6 @@ _logger = DBLogger(__name__) class ExecutedMigrationDao(DataAccessObjectABC[ExecutedMigration]): def __init__(self): - DataAccessObjectABC.__init__(self, __name__, ExecutedMigration, InternalTables.executed_migrations) + DataAccessObjectABC.__init__(self, __name__, ExecutedMigration, TableManager.get("executed_migrations")) self.attribute(ExecutedMigration.migration_id, str, primary_key=True, db_name="migrationId") diff --git a/src/cpl-database/cpl/database/scripts/mysql/0-cpl-initial.sql b/src/cpl-database/cpl/database/scripts/mysql/0-cpl-initial.sql index d2a1b292..67d94688 100644 --- a/src/cpl-database/cpl/database/scripts/mysql/0-cpl-initial.sql +++ b/src/cpl-database/cpl/database/scripts/mysql/0-cpl-initial.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS _executed_migrations +CREATE TABLE IF NOT EXISTS system__executed_migrations ( migrationId VARCHAR(255) PRIMARY KEY, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/src/cpl-database/cpl/database/scripts/mysql/trigger.txt b/src/cpl-database/cpl/database/scripts/mysql/trigger.txt index 9ce1cf14..41c21fb3 100644 --- a/src/cpl-database/cpl/database/scripts/mysql/trigger.txt +++ b/src/cpl-database/cpl/database/scripts/mysql/trigger.txt @@ -1,26 +1,21 @@ +DROP TRIGGER IF EXISTS `TR_TableUpdate`; -DELIMITER // -CREATE TRIGGER mytable_before_update - BEFORE UPDATE - ON mytable +CREATE TRIGGER `TR_TableUpdate` + AFTER UPDATE + ON `Table` FOR EACH ROW BEGIN - INSERT INTO mytable_history - SELECT OLD.*; - - SET NEW.updated = NOW(); + INSERT INTO `TableHistory` (Id, ..., Deleted, EditorId, Created, Updated) + VALUES (OLD.Id, ..., OLD.Deleted, OLD.Created, CURRENT_TIMESTAMP()); END; -// -DELIMITER ; -DELIMITER // -CREATE TRIGGER mytable_before_delete - BEFORE DELETE - ON mytable +DROP TRIGGER IF EXISTS `TR_TableDelete`; + +CREATE TRIGGER `TR_TableDelete` + AFTER DELETE + ON `Table` FOR EACH ROW BEGIN - INSERT INTO mytable_history - SELECT OLD.*; -END; -// -DELIMITER ; \ No newline at end of file + INSERT INTO `TableHistory` (Id, ..., Deleted, EditorId, Created, Updated) + VALUES (OLD.Id, ..., TRUE, OLD.Created, CURRENT_TIMESTAMP()); +END; \ No newline at end of file diff --git a/src/cpl-database/cpl/database/scripts/postgres/0-cpl-initial.sql b/src/cpl-database/cpl/database/scripts/postgres/0-cpl-initial.sql index 3857a6e7..1010462f 100644 --- a/src/cpl-database/cpl/database/scripts/postgres/0-cpl-initial.sql +++ b/src/cpl-database/cpl/database/scripts/postgres/0-cpl-initial.sql @@ -3,9 +3,9 @@ CREATE SCHEMA IF NOT EXISTS system; CREATE TABLE IF NOT EXISTS system._executed_migrations ( - MigrationId VARCHAR(255) PRIMARY KEY, - Created timestamptz NOT NULL DEFAULT NOW(), - Updated timestamptz NOT NULL DEFAULT NOW() + migrationId VARCHAR(255) PRIMARY KEY, + created timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW() ); CREATE OR REPLACE FUNCTION public.history_trigger_function() @@ -33,9 +33,9 @@ BEGIN USING OLD; END IF; - -- For UPDATE, update the Updated column and return the new row + -- For UPDATE, update the updated column and return the new row IF (TG_OP = 'UPDATE') THEN - NEW.updated := NOW(); -- Update the Updated column + NEW.updated := NOW(); -- Update the updated column RETURN NEW; END IF; diff --git a/src/cpl-database/cpl/database/service/seeder_service.py b/src/cpl-database/cpl/database/service/seeder_service.py new file mode 100644 index 00000000..5b0b8fba --- /dev/null +++ b/src/cpl-database/cpl/database/service/seeder_service.py @@ -0,0 +1,18 @@ +from cpl.database.abc.data_seeder_abc import DataSeederABC +from cpl.database.db_logger import DBLogger +from cpl.dependency import ServiceProviderABC + + +_logger = DBLogger(__name__) + + +class SeederService: + + def __init__(self, provider: ServiceProviderABC): + self._provider = provider + + async def seed(self): + seeders = self._provider.get_services(DataSeederABC) + _logger.debug(f"Found {len(seeders)} seeders") + for seeder in seeders: + await seeder.seed() diff --git a/src/cpl-database/cpl/database/table_manager.py b/src/cpl-database/cpl/database/table_manager.py new file mode 100644 index 00000000..1d7ad7a1 --- /dev/null +++ b/src/cpl-database/cpl/database/table_manager.py @@ -0,0 +1,49 @@ +from cpl.database.model.server_type import ServerTypes, ServerType + + +class TableManager: + _tables: dict[str, dict[ServerType, str]] = { + "executed_migrations": { + ServerTypes.POSTGRES: "system._executed_migrations", + ServerTypes.MYSQL: "system__executed_migrations", + }, + "auth_users": { + ServerTypes.POSTGRES: "administration.auth_users", + ServerTypes.MYSQL: "administration_auth_users", + }, + "api_keys": { + ServerTypes.POSTGRES: "administration.api_keys", + ServerTypes.MYSQL: "administration_api_keys", + }, + "api_key_permissions": { + ServerTypes.POSTGRES: "permission.api_key_permissions", + ServerTypes.MYSQL: "permission_api_key_permissions", + }, + "permissions": { + ServerTypes.POSTGRES: "permission.permissions", + ServerTypes.MYSQL: "permission_permissions", + }, + "roles": { + ServerTypes.POSTGRES: "permission.roles", + ServerTypes.MYSQL: "permission_roles", + }, + "role_permissions": { + ServerTypes.POSTGRES: "permission.role_permissions", + ServerTypes.MYSQL: "permission_role_permissions", + }, + "role_users": { + ServerTypes.POSTGRES: "permission.role_users", + ServerTypes.MYSQL: "permission_role_users", + }, + } + + @classmethod + def get(cls, key: str) -> str: + if key not in cls._tables: + raise KeyError(f"Table '{key}' not found in TableManager.") + + server_type = ServerType.server_type + if server_type not in cls._tables[key]: + raise KeyError(f"Server type '{server_type}' not configured for table '{key}'.") + + return cls._tables[key][server_type] diff --git a/src/cpl-dependency/cpl/dependency/service_provider.py b/src/cpl-dependency/cpl/dependency/service_provider.py index f3c141ee..28e54cbb 100644 --- a/src/cpl-dependency/cpl/dependency/service_provider.py +++ b/src/cpl-dependency/cpl/dependency/service_provider.py @@ -59,7 +59,7 @@ class ServiceProvider(ServiceProviderABC): # raise Exception(f'Service {parameter.annotation} not found') - def _get_services(self, t: type, *args, service_type: type = None, **kwargs) -> list[Optional[object]]: + def _get_services(self, t: type, service_type: type = None, **kwargs) -> list[Optional[object]]: implementations = [] for descriptor in self._service_descriptors: if descriptor.service_type == t or issubclass(descriptor.service_type, t): @@ -67,7 +67,9 @@ class ServiceProvider(ServiceProviderABC): implementations.append(descriptor.implementation) continue - implementation = self._build_service(descriptor.service_type, *args, service_type, **kwargs) + implementation = self._build_service( + descriptor.service_type, origin_service_type=service_type, **kwargs + ) if descriptor.lifetime == ServiceLifetimeEnum.singleton: descriptor.implementation = implementation @@ -93,7 +95,8 @@ class ServiceProvider(ServiceProviderABC): params.append(Environment) elif issubclass(parameter.annotation, ConfigurationModelABC): - params.append(Configuration.get(parameter.annotation)) + conf = Configuration.get(parameter.annotation) + params.append(parameter.annotation() if conf is None else conf) elif issubclass(parameter.annotation, Configuration): params.append(Configuration) diff --git a/src/cpl-dependency/cpl/dependency/service_provider_abc.py b/src/cpl-dependency/cpl/dependency/service_provider_abc.py index d6e06b36..8e3220e7 100644 --- a/src/cpl-dependency/cpl/dependency/service_provider_abc.py +++ b/src/cpl-dependency/cpl/dependency/service_provider_abc.py @@ -3,8 +3,8 @@ from abc import abstractmethod, ABC from inspect import Signature, signature from typing import Optional -from cpl.dependency.scope_abc import ScopeABC from cpl.core.typing import T, R +from cpl.dependency.scope_abc import ScopeABC class ServiceProviderABC(ABC): @@ -24,6 +24,18 @@ class ServiceProviderABC(ABC): def get_global_provider(cls) -> Optional["ServiceProviderABC"]: return cls._provider + @classmethod + def get_global_service(cls, instance_type: T, *args, **kwargs) -> Optional[R]: + if cls._provider is None: + return None + return cls._provider.get_service(instance_type, *args, **kwargs) + + @classmethod + def get_global_services(cls, instance_type: T, *args, **kwargs) -> list[Optional[R]]: + if cls._provider is None: + return [] + return cls._provider.get_services(instance_type, *args, **kwargs) + @abstractmethod def _build_by_signature(self, sig: Signature, origin_service_type: type) -> list[R]: pass diff --git a/src/cpl-mail/requirements.txt b/src/cpl-mail/requirements.txt index a8244b30..e8d9db7b 100644 --- a/src/cpl-mail/requirements.txt +++ b/src/cpl-mail/requirements.txt @@ -1 +1,2 @@ -cpl-core \ No newline at end of file +cpl-core +cpl-dependency \ No newline at end of file diff --git a/src/cpl-translation/requirements.txt b/src/cpl-translation/requirements.txt index a8244b30..e8d9db7b 100644 --- a/src/cpl-translation/requirements.txt +++ b/src/cpl-translation/requirements.txt @@ -1 +1,2 @@ -cpl-core \ No newline at end of file +cpl-core +cpl-dependency \ No newline at end of file diff --git a/tests/custom/database/src/application.py b/tests/custom/database/src/application.py index 0bbb49e9..6343c77b 100644 --- a/tests/custom/database/src/application.py +++ b/tests/custom/database/src/application.py @@ -1,13 +1,15 @@ from typing import Optional from cpl.application import ApplicationABC +from cpl.auth import KeycloakAdmin from cpl.core.console import Console from cpl.core.environment import Environment from cpl.core.log import LoggerABC from cpl.dependency import ServiceProviderABC +from model.city import City +from model.city_dao import CityDao +from model.user import User from model.user_dao import UserDao -from model.user_repo import UserRepo -from model.user_repo_abc import UserRepoABC class Application(ApplicationABC): @@ -16,21 +18,16 @@ class Application(ApplicationABC): self._logger: Optional[LoggerABC] = None - async def test_repos(self): - user_repo: UserRepo = self._services.get_service(UserRepoABC) - if len(await user_repo.get_users()) == 0: - user_repo.add_test_user() - - Console.write_line("Users:") - for user in await user_repo.get_users(): - Console.write_line(user.UserId, user.Name, user.City) - - Console.write_line("Cities:") - for city in await user_repo.get_cities(): - Console.write_line(city.CityId, city.Name, city.ZIP) - async def test_daos(self): userDao: UserDao = self._services.get_service(UserDao) + cityDao: CityDao = self._services.get_service(CityDao) + + Console.write_line(await userDao.get_all()) + + if len(await cityDao.get_all()) == 0: + city_id = await cityDao.create(City(0, "Haren", "49733")) + await userDao.create(User(0, "NewUser", city_id)) + Console.write_line(await userDao.get_all()) async def configure(self): @@ -41,3 +38,7 @@ class Application(ApplicationABC): self._logger.debug(f"Environment: {Environment.get_environment()}") await self.test_daos() + + kc_admin: KeycloakAdmin = self._services.get_service(KeycloakAdmin) + x = kc_admin.get_users() + Console.write_line(x) diff --git a/tests/custom/database/src/custom_permissions.py b/tests/custom/database/src/custom_permissions.py new file mode 100644 index 00000000..fd6254d0 --- /dev/null +++ b/tests/custom/database/src/custom_permissions.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class CustomPermissions(Enum): + test = "test" diff --git a/tests/custom/database/src/main.py b/tests/custom/database/src/main.py index 86abcbc0..0fcf389d 100644 --- a/tests/custom/database/src/main.py +++ b/tests/custom/database/src/main.py @@ -13,4 +13,5 @@ async def main(): if __name__ == "__main__": import asyncio - asyncio.run(main()) + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/tests/custom/database/src/model/city.py b/tests/custom/database/src/model/city.py new file mode 100644 index 00000000..c98bef85 --- /dev/null +++ b/tests/custom/database/src/model/city.py @@ -0,0 +1,29 @@ +from datetime import datetime +from typing import Optional + +from cpl.core.typing import SerialId +from cpl.database.abc.db_model_abc import DbModelABC + + +class City(DbModelABC): + def __init__( + self, + id: int, + name: str, + zip: str, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._name = name + self._zip = zip + + @property + def name(self) -> str: + return self._name + + @property + def zip(self) -> str: + return self._zip diff --git a/tests/custom/database/src/model/city_dao.py b/tests/custom/database/src/model/city_dao.py new file mode 100644 index 00000000..bea1ba85 --- /dev/null +++ b/tests/custom/database/src/model/city_dao.py @@ -0,0 +1,11 @@ +from cpl.database.abc import DbModelDaoABC +from model.city import City + + +class CityDao(DbModelDaoABC[City]): + + def __init__(self): + DbModelDaoABC.__init__(self, __name__, City, "city") + + self.attribute(City.name, str) + self.attribute(City.zip, int) diff --git a/tests/custom/database/src/model/city_model.py b/tests/custom/database/src/model/city_model.py deleted file mode 100644 index f56bc8c7..00000000 --- a/tests/custom/database/src/model/city_model.py +++ /dev/null @@ -1,54 +0,0 @@ -from cpl.database.abc.table_abc import TableABC - - -class CityModel(TableABC): - def __init__(self, name: str, zip_code: str, id=0): - self.CityId = id - self.Name = name - self.ZIP = zip_code - - @staticmethod - def get_create_string() -> str: - return str( - f""" - CREATE TABLE IF NOT EXISTS `City` ( - `CityId` INT(30) NOT NULL AUTO_INCREMENT, - `Name` VARCHAR(64) NOT NULL, - `ZIP` VARCHAR(5) NOT NULL, - PRIMARY KEY(`CityId`) - ); - """ - ) - - @property - def insert_string(self) -> str: - return str( - f""" - INSERT INTO `City` ( - `Name`, `ZIP` - ) VALUES ( - '{self.Name}', - '{self.ZIP}' - ); - """ - ) - - @property - def udpate_string(self) -> str: - return str( - f""" - UPDATE `City` - SET `Name` = '{self.Name}', - `ZIP` = '{self.ZIP}', - WHERE `CityId` = {self.Id}; - """ - ) - - @property - def delete_string(self) -> str: - return str( - f""" - DELETE FROM `City` - WHERE `CityId` = {self.Id}; - """ - ) diff --git a/tests/custom/database/src/model/user.py b/tests/custom/database/src/model/user.py index 51b7ee18..445c56b7 100644 --- a/tests/custom/database/src/model/user.py +++ b/tests/custom/database/src/model/user.py @@ -1,9 +1,23 @@ +from datetime import datetime +from typing import Optional + +from cpl.core.typing import SerialId from cpl.database.abc.db_model_abc import DbModelABC class User(DbModelABC): - def __init__(self, id: int, name: str, city_id: int = 0): - DbModelABC.__init__(self, id) + + def __init__( + self, + id: int, + name: str, + city_id: int = 0, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) self._name = name self._city_id = city_id @@ -13,4 +27,4 @@ class User(DbModelABC): @property def city_id(self) -> int: - return self._city_id \ No newline at end of file + return self._city_id diff --git a/tests/custom/database/src/model/user_dao.py b/tests/custom/database/src/model/user_dao.py index e4a0a3ba..48267dec 100644 --- a/tests/custom/database/src/model/user_dao.py +++ b/tests/custom/database/src/model/user_dao.py @@ -1,4 +1,3 @@ -from cpl.database import InternalTables from cpl.database.abc import DbModelDaoABC from model.user import User @@ -6,7 +5,7 @@ from model.user import User class UserDao(DbModelDaoABC[User]): def __init__(self): - DbModelDaoABC.__init__(self, __name__, User, InternalTables.users) + DbModelDaoABC.__init__(self, __name__, User, "users") self.attribute(User.name, str) self.attribute(User.city_id, int, db_name="CityId") diff --git a/tests/custom/database/src/model/user_model.py b/tests/custom/database/src/model/user_model.py deleted file mode 100644 index 25b07e2d..00000000 --- a/tests/custom/database/src/model/user_model.py +++ /dev/null @@ -1,56 +0,0 @@ -from cpl.database.abc.table_abc import TableABC -from .city_model import CityModel - - -class UserModel(TableABC): - def __init__(self, name: str, city: CityModel, id=0): - self.UserId = id - self.Name = name - self.CityId = city.CityId if city is not None else 0 - self.City = city - - @staticmethod - def get_create_string() -> str: - return str( - f""" - CREATE TABLE IF NOT EXISTS `User` ( - `UserId` INT(30) NOT NULL AUTO_INCREMENT, - `Name` VARCHAR(64) NOT NULL, - `CityId` INT(30), - FOREIGN KEY (`UserId`) REFERENCES City(`CityId`), - PRIMARY KEY(`UserId`) - ); - """ - ) - - @property - def insert_string(self) -> str: - return str( - f""" - INSERT INTO `User` ( - `Name` - ) VALUES ( - '{self.Name}' - ); - """ - ) - - @property - def udpate_string(self) -> str: - return str( - f""" - UPDATE `User` - SET `Name` = '{self.Name}', - `CityId` = {self.CityId}, - WHERE `UserId` = {self.UserId}; - """ - ) - - @property - def delete_string(self) -> str: - return str( - f""" - DELETE FROM `User` - WHERE `UserId` = {self.UserId}; - """ - ) diff --git a/tests/custom/database/src/model/user_repo.py b/tests/custom/database/src/model/user_repo.py deleted file mode 100644 index 806c2209..00000000 --- a/tests/custom/database/src/model/user_repo.py +++ /dev/null @@ -1,38 +0,0 @@ -from cpl.database.abc.db_context_abc import DBContextABC -from .city_model import CityModel -from .user_model import UserModel -from .user_repo_abc import UserRepoABC - - -class UserRepo(UserRepoABC): - def __init__(self, db_context: DBContextABC): - UserRepoABC.__init__(self) - - self._db_context: DBContextABC = db_context - - def add_test_user(self): - city = CityModel("Haren", "49733") - city2 = CityModel("Meppen", "49716") - self._db_context.execute(city2.insert_string) - user = UserModel("TestUser", city) - self._db_context.execute(user.insert_string) - - async def get_users(self) -> list[UserModel]: - users = [] - results = await self._db_context.select("SELECT * FROM `User`") - for result in results: - users.append(UserModel(result[1], await self.get_city_by_id(result[2]), id=result[0])) - return users - - async def get_cities(self) -> list[CityModel]: - cities = [] - results = await self._db_context.select("SELECT * FROM `City`") - for result in results: - cities.append(CityModel(result[1], result[2], id=result[0])) - return cities - - async def get_city_by_id(self, id: int) -> CityModel: - if id is None: - return None - result = await self._db_context.select(f"SELECT * FROM `City` WHERE `Id` = {id}") - return CityModel(result[1], result[2], id=result[0]) diff --git a/tests/custom/database/src/model/user_repo_abc.py b/tests/custom/database/src/model/user_repo_abc.py deleted file mode 100644 index 0e4d3abe..00000000 --- a/tests/custom/database/src/model/user_repo_abc.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import ABC, abstractmethod - -from .city_model import CityModel -from .user_model import UserModel - - -class UserRepoABC(ABC): - @abstractmethod - def __init__(self): - pass - - @abstractmethod - def get_users(self) -> list[UserModel]: - pass - - @abstractmethod - def get_cities(self) -> list[CityModel]: - pass - - @abstractmethod - def get_city_by_id(self, id: int) -> CityModel: - pass diff --git a/tests/custom/database/src/scripts/0-initial.sql b/tests/custom/database/src/scripts/0-initial.sql index 7fa8584f..ead26fd7 100644 --- a/tests/custom/database/src/scripts/0-initial.sql +++ b/tests/custom/database/src/scripts/0-initial.sql @@ -2,6 +2,10 @@ CREATE TABLE IF NOT EXISTS `city` ( `id` INT(30) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `zip` VARCHAR(5) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(`id`) ); @@ -9,6 +13,10 @@ CREATE TABLE IF NOT EXISTS `users` ( `id` INT(30) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `cityId` INT(30), + deleted BOOLEAN NOT NULL DEFAULT FALSE, + editorId INT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (`cityId`) REFERENCES city(`id`), PRIMARY KEY(`id`) ); \ No newline at end of file diff --git a/tests/custom/database/src/startup.py b/tests/custom/database/src/startup.py index c9ccfec5..d05782b5 100644 --- a/tests/custom/database/src/startup.py +++ b/tests/custom/database/src/startup.py @@ -1,14 +1,18 @@ +from cpl import auth from cpl.application.async_startup_abc import AsyncStartupABC +from cpl.auth import permission +from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.core.configuration import Configuration from cpl.core.environment import Environment from cpl.core.log import Logger, LoggerABC from cpl.database import mysql from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.service.migration_service import MigrationService +from cpl.database.service.seeder_service import SeederService from cpl.dependency import ServiceCollection +from custom_permissions import CustomPermissions +from model.city_dao import CityDao from model.user_dao import UserDao -from model.user_repo import UserRepo -from model.user_repo_abc import UserRepoABC class Startup(AsyncStartupABC): @@ -22,14 +26,20 @@ class Startup(AsyncStartupABC): async def configure_services(self, services: ServiceCollection, environment: Environment): services.add_module(mysql) + services.add_module(auth) + services.add_module(permission) services.add_transient(DataAccessObjectABC, UserDao) + services.add_transient(DataAccessObjectABC, CityDao) - services.add_singleton(UserRepoABC, UserRepo) services.add_singleton(LoggerABC, Logger) + PermissionsRegistry.with_enum(CustomPermissions) + provider = services.build_service_provider() migration_service: MigrationService = provider.get_service(MigrationService) migration_service.with_directory("./scripts") await migration_service.migrate() + seeder_service: SeederService = provider.get_service(SeederService) + await seeder_service.seed()