diff --git a/src/cpl-graphql/cpl/graphql/application/graphql_app.py b/src/cpl-graphql/cpl/graphql/application/graphql_app.py index 13e688c3..207c1861 100644 --- a/src/cpl-graphql/cpl/graphql/application/graphql_app.py +++ b/src/cpl-graphql/cpl/graphql/application/graphql_app.py @@ -1,20 +1,21 @@ +import socket from enum import Enum from typing import Self from cpl.api.application import WebApp from cpl.api.model.validation_match import ValidationMatch +from cpl.auth.schema import UserDao +from cpl.core.configuration import Configuration +from cpl.core.environment import Environment from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.typing import Modules from queries.user import UserGraphType, UserFilter, UserSort -from .._endpoints.graphiql import graphiql_endpoint -from .._endpoints.graphql import graphql_endpoint -from .._endpoints.playground import playground_endpoint -from ..auth.administration.user.user_mutation import UserMutation -from ..graphql_module import GraphQLModule -from ..service.schema import Schema -from ...application.abc.application_abc import __not_implemented__ -from ...auth.schema import UserDao -from ...core.configuration import Configuration +from cpl.graphql._endpoints.graphiql import graphiql_endpoint +from cpl.graphql._endpoints.graphql import graphql_endpoint +from cpl.graphql._endpoints.playground import playground_endpoint +from cpl.graphql.auth.administration.user.user_mutation import UserMutation +from cpl.graphql.graphql_module import GraphQLModule +from cpl.graphql.service.schema import Schema class GraphQLApp(WebApp): @@ -104,8 +105,14 @@ class GraphQLApp(WebApp): schema.mutation.with_mutation("user", UserMutation).with_public(public) async def _log_before_startup(self): - self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}") + host = self._api_settings.host + if host == "0.0.0.0" and Environment.get_environment() == "development": + host = "localhost" + elif host == "0.0.0.0": + host = socket.gethostbyname(socket.gethostname()) + + self._logger.info(f"Start API on {host}:{self._api_settings.port}") if self._with_graphiql: - self._logger.warning(f"GraphiQL available at http://{self._api_settings.host}:{self._api_settings.port}/api/graphiql") + self._logger.warning(f"GraphiQL available at http://{host}:{self._api_settings.port}/api/graphiql") if self._with_playground: - self._logger.warning(f"GraphQL Playground available at http://{self._api_settings.host}:{self._api_settings.port}/api/playground") + self._logger.warning(f"GraphQL Playground available at http://{host}:{self._api_settings.port}/api/playground") diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/api_key/__init__.py b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_filter.py b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_filter.py new file mode 100644 index 00000000..9c5752d2 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_filter.py @@ -0,0 +1,10 @@ +from cpl.auth.schema import ApiKey +from cpl.graphql.schema.filter.db_model_filter import DbModelFilter +from cpl.graphql.schema.filter.string_filter import StringFilter + + +class ApiKeyFilter(DbModelFilter[ApiKey]): + def __init__(self, public: bool = False): + DbModelFilter.__init__(self, public) + + self.field("identifier", StringFilter).with_public(public) diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_graph_type.py b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_graph_type.py new file mode 100644 index 00000000..c70959a1 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_graph_type.py @@ -0,0 +1,14 @@ +from cpl.auth.schema import ApiKey, RolePermissionDao +from cpl.graphql.schema.db_model_graph_type import DbModelGraphType + + +class ApiKeyGraphType(DbModelGraphType): + + def __init__(self, role_permission_dao: RolePermissionDao): + DbModelGraphType.__init__(self) + + self.string_field(ApiKey.identifier, lambda root: root.identifier) + self.string_field(ApiKey.key, lambda root: root.key) + self.string_field(ApiKey.permissions, lambda root: root.permissions) + + self.set_history_reference_dao(role_permission_dao, "apikeyid") diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_input.py b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_input.py new file mode 100644 index 00000000..a669fce1 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_input.py @@ -0,0 +1,25 @@ +from cpl.auth.schema import ApiKey +from cpl.core.typing import SerialId +from cpl.graphql.schema.input import Input + + +class ApiKeyCreateInput(Input[ApiKey]): + identifier: str + permissions: list[SerialId] + + def __init__(self): + Input.__init__(self) + self.string_field("identifier").with_required() + self.list_field("permissions", SerialId) + + +class ApiKeyUpdateInput(Input[ApiKey]): + id: SerialId + identifier: str | None + permissions: list[SerialId] | None + + def __init__(self): + Input.__init__(self) + self.int_field("id").with_required() + self.string_field("identifier").with_required() + self.list_field("permissions", SerialId) diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_mutation.py b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_mutation.py new file mode 100644 index 00000000..ea2f9cf1 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/auth/administration/api_key/api_key_mutation.py @@ -0,0 +1,96 @@ +from cpl.api import APILogger +from cpl.auth.keycloak import KeycloakAdmin +from cpl.auth.permission import Permissions +from cpl.auth.schema import ApiKey, ApiKeyDao, ApiKeyPermissionDao, ApiKeyPermission +from cpl.graphql.auth.administration.api_key.api_key_input import ApiKeyUpdateInput, ApiKeyCreateInput +from cpl.graphql.schema.mutation import Mutation + + +class ApiKeyMutation(Mutation): + def __init__( + self, + + logger: APILogger, + api_key_dao: ApiKeyDao, + api_key_permission_dao: ApiKeyPermissionDao, + permission_dao: ApiKeyPermissionDao, + keycloak_admin: KeycloakAdmin, + ): + Mutation.__init__(self) + self._logger = logger + self._api_key_dao = api_key_dao + self._api_key_permission_dao = api_key_permission_dao + self._permission_dao = permission_dao + self._keycloak_admin = keycloak_admin + + self.int_field( + "create", + self.resolve_create, + ).with_require_any_permission(Permissions.users_create).with_argument( + "input", + ApiKeyCreateInput, + ).with_required() + + self.bool_field( + "update", + self.resolve_update, + ).with_require_any_permission(Permissions.users_update).with_argument( + "input", + ApiKeyUpdateInput, + ).with_required() + + self.bool_field( + "delete", + self.resolve_delete, + ).with_require_any_permission(Permissions.users_delete).with_argument( + "id", + int, + ).with_required() + + self.bool_field( + "restore", + self.resolve_restore, + ).with_require_any_permission(Permissions.users_delete).with_argument( + "id", + int, + ).with_required() + + async def resolve_create(self, obj: ApiKeyCreateInput): + self._logger.debug(f"create api key: {obj.__dict__}") + + api_key = ApiKey.new(obj.identifier) + await self._api_key_dao.create(api_key) + api_key = await self._api_key_dao.get_single_by([{ApiKey.identifier: obj.identifier}]) + await self._api_key_permission_dao.create_many( + [ApiKeyPermission(0, api_key.id, x) for x in obj.permissions] + ) + return api_key + + async def resolve_update(self, input: ApiKeyUpdateInput): + self._logger.debug(f"update api key: {input}") + api_key = await self._api_key_dao.get_by_id(input.id) + + await self._resolve_assignments( + input.permissions or [], + api_key, + ApiKeyPermission.api_key_id, + ApiKeyPermission.permission_id, + self._api_key_dao, + self._api_key_permission_dao, + ApiKeyPermission, + self._permission_dao, + ) + + return api_key + + async def resolve_delete(self, id: str): + self._logger.debug(f"delete api key: {id}") + api_key = await self._api_key_dao.get_by_id(id) + await self._api_key_dao.delete(api_key) + return True + + async def resolve_restore(self, id: str): + self._logger.debug(f"restore api key: {id}") + api_key = await self._api_key_dao.get_by_id(id) + await self._api_key_dao.restore(api_key) + return True diff --git a/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py index be46dd10..c5f5ac07 100644 --- a/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py +++ b/src/cpl-graphql/cpl/graphql/auth/administration/user/user_input.py @@ -5,7 +5,7 @@ from cpl.graphql.schema.input import Input class UserCreateInput(Input[User]): keycloak_id: str - roles: list[SerialId] + roles: list[SerialId] | None def __init__(self): Input.__init__(self) @@ -15,7 +15,7 @@ class UserCreateInput(Input[User]): class UserUpdateInput(Input[User]): id: SerialId - roles: list[SerialId] + roles: list[SerialId] | None def __init__(self): Input.__init__(self) diff --git a/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py b/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py index e340eaeb..ba0da432 100644 --- a/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py +++ b/src/cpl-graphql/cpl/graphql/auth/graphql_auth_module.py @@ -2,6 +2,9 @@ from cpl.core.configuration import Configuration from cpl.dependency import ServiceProvider from cpl.dependency.module.module import Module from cpl.dependency.service_collection import ServiceCollection +from cpl.graphql.auth.administration.api_key.api_key_filter import ApiKeyFilter +from cpl.graphql.auth.administration.api_key.api_key_graph_type import ApiKeyGraphType +from cpl.graphql.auth.administration.api_key.api_key_mutation import ApiKeyMutation from cpl.graphql.auth.administration.user.user_filter import UserFilter from cpl.graphql.auth.administration.user.user_graph_type import UserGraphType from cpl.graphql.auth.administration.user.user_mutation import UserMutation @@ -11,7 +14,7 @@ from cpl.graphql.service.schema import Schema class GraphQLAuthModule(Module): dependencies = [GraphQLModule] - transient = [UserGraphType, UserMutation, UserFilter] + transient = [UserGraphType, UserMutation, UserFilter, ApiKeyGraphType, ApiKeyMutation, ApiKeyFilter] @staticmethod def register(collection: ServiceCollection): @@ -21,5 +24,3 @@ class GraphQLAuthModule(Module): def configure(provider: ServiceProvider): schema = provider.get_service(Schema) schema.with_type(UserGraphType) - -