diff --git a/karp-backend/src/karp/auth_infrastructure/__init__.py b/karp-backend/src/karp/auth_infrastructure/__init__.py index 14726e1a..899b4060 100644 --- a/karp-backend/src/karp/auth_infrastructure/__init__.py +++ b/karp-backend/src/karp/auth_infrastructure/__init__.py @@ -2,28 +2,20 @@ import injector from karp.auth_infrastructure.queries import ( - LexGetResourcePermissions, - LexIsResourceProtected, + ResourcePermissionQueries, ) from karp.auth_infrastructure.services import ( JWTAuthService, - JWTAuthServiceConfig, ) -from karp.lex_infrastructure import ResourceQueries +from karp.lex.application.repositories import ResourceRepository class AuthInfrastructure(injector.Module): # noqa: D101 @injector.provider def resource_permissions( # noqa: D102 - self, resources: ResourceQueries - ) -> LexGetResourcePermissions: - return LexGetResourcePermissions(resources) - - @injector.provider - def is_resource_protected( # noqa: D102 - self, resources: ResourceQueries - ) -> LexIsResourceProtected: - return LexIsResourceProtected(resources) + self, resources: ResourceRepository + ) -> ResourcePermissionQueries: + return ResourcePermissionQueries(resources) class JwtAuthInfrastructure(injector.Module): # noqa: D101 @@ -32,14 +24,5 @@ def __init__(self, pubkey_path: Path) -> None: # noqa: D107 self.pubkey_path = pubkey_path @injector.provider - @injector.singleton - def jwt_auth_service_config(self) -> JWTAuthServiceConfig: # noqa: D102 - return JWTAuthServiceConfig(self.pubkey_path) - - @injector.provider - def jwt_auth_service( # noqa: D102 - self, is_resource_protected: LexIsResourceProtected - ) -> JWTAuthService: - return JWTAuthService( - pubkey_path=self.pubkey_path, is_resource_protected=is_resource_protected - ) + def jwt_auth_service(self) -> JWTAuthService: + return JWTAuthService(pubkey_path=self.pubkey_path) diff --git a/karp-backend/src/karp/auth_infrastructure/queries/__init__.py b/karp-backend/src/karp/auth_infrastructure/queries/__init__.py index 3700cf7d..c5223e45 100644 --- a/karp-backend/src/karp/auth_infrastructure/queries/__init__.py +++ b/karp-backend/src/karp/auth_infrastructure/queries/__init__.py @@ -1,4 +1,3 @@ from .lex_resources import ( - LexGetResourcePermissions, - LexIsResourceProtected, + ResourcePermissionQueries, ) diff --git a/karp-backend/src/karp/auth_infrastructure/queries/lex_resources.py b/karp-backend/src/karp/auth_infrastructure/queries/lex_resources.py index 5d992075..07a34bf3 100644 --- a/karp-backend/src/karp/auth_infrastructure/queries/lex_resources.py +++ b/karp-backend/src/karp/auth_infrastructure/queries/lex_resources.py @@ -2,15 +2,16 @@ from karp.auth.application.queries.resources import ResourcePermissionDto from karp.auth.domain import errors +from karp.auth.domain.entities.user import User from karp.foundation.value_objects.permission_level import PermissionLevel -from karp.lex_infrastructure import ResourceQueries +from karp.lex.application.repositories import ResourceRepository -class LexGetResourcePermissions: - def __init__(self, resources: ResourceQueries): +class ResourcePermissionQueries: + def __init__(self, resources: ResourceRepository): self.resources = resources - def query(self) -> typing.List[ResourcePermissionDto]: # noqa: D102 + def get_resource_permissions(self) -> typing.List[ResourcePermissionDto]: # noqa: D102 resource_permissions = [] for resource in self.resources.get_published_resources(): resource_obj = {"resource_id": resource.resource_id} @@ -31,16 +32,22 @@ def query(self) -> typing.List[ResourcePermissionDto]: # noqa: D102 return resource_permissions - -class LexIsResourceProtected: - def __init__(self, resource_queries: ResourceQueries) -> None: # noqa: D107 - super().__init__() - self.resource_queries = resource_queries - - def query(self, resource_id: str, level: PermissionLevel) -> bool: # noqa: D102 + def is_resource_protected(self, resource_id: str, level: PermissionLevel) -> bool: # noqa: D102 if level in [PermissionLevel.write, PermissionLevel.admin]: return True - resource = self.resource_queries.by_resource_id_optional(resource_id=resource_id) + resource = self.resources.by_resource_id_optional(resource_id=resource_id) if not resource: raise errors.ResourceNotFound(f"Can't find resource '{resource_id}'") return resource.config.get("protected", {}).get("read", False) + + def has_permission( # noqa: ANN201, D102 + self, + level: PermissionLevel, + user: User, + resource_ids: typing.List[str], + ) -> bool: + return not any( + self.is_resource_protected(resource_id, level) + and (not user or not user.has_enough_permissions(resource_id, level)) + for resource_id in resource_ids + ) diff --git a/karp-backend/src/karp/auth_infrastructure/services/jwt_auth_service.py b/karp-backend/src/karp/auth_infrastructure/services/jwt_auth_service.py index 1312cf89..1ed89bee 100644 --- a/karp-backend/src/karp/auth_infrastructure/services/jwt_auth_service.py +++ b/karp-backend/src/karp/auth_infrastructure/services/jwt_auth_service.py @@ -7,7 +7,7 @@ import pydantic import logging -from karp.auth_infrastructure import LexIsResourceProtected +from karp.auth_infrastructure import ResourcePermissionQueries from karp.foundation import value_objects from karp.auth.domain.errors import ExpiredToken, TokenError, InvalidTokenPayload from karp.auth.domain.entities.user import User @@ -49,10 +49,9 @@ def pubkey_path(self) -> Path: # noqa: D102 class JWTAuthService: def __init__( # noqa: D107 - self, pubkey_path: Path, is_resource_protected: LexIsResourceProtected + self, pubkey_path: Path ) -> None: self._jwt_key = load_jwt_key(pubkey_path) - self.is_resource_protected = is_resource_protected logger.debug("JWTAuthenticator created") def authenticate(self, _scheme: str, credentials: str) -> User: # noqa: D102 @@ -76,15 +75,3 @@ def authenticate(self, _scheme: str, credentials: str) -> User: # noqa: D102 if payload.scope and "lexica" in payload.scope: lexicon_permissions = payload.scope["lexica"] return User(payload.sub, lexicon_permissions, payload.levels) - - def authorize( # noqa: ANN201, D102 - self, - level: value_objects.PermissionLevel, - user: User, - resource_ids: List[str], - ): - return not any( - self.is_resource_protected.query(resource_id, level) - and (not user or not user.has_enough_permissions(resource_id, level)) - for resource_id in resource_ids - ) diff --git a/karp-backend/src/karp/karp_v6_api/dependencies/auth_deps.py b/karp-backend/src/karp/karp_v6_api/dependencies/auth_deps.py index a93b1fd9..4801e374 100644 --- a/karp-backend/src/karp/karp_v6_api/dependencies/auth_deps.py +++ b/karp-backend/src/karp/karp_v6_api/dependencies/auth_deps.py @@ -9,13 +9,11 @@ from karp.auth.domain.errors import TokenError from karp.auth_infrastructure import ( JWTAuthService, - JWTAuthServiceConfig, - LexGetResourcePermissions, - LexIsResourceProtected, + ResourcePermissionQueries, ) from karp.main.errors import KarpError # noqa: F401 -from ...lex_infrastructure import ResourceQueries +from karp.lex.application.repositories import ResourceRepository from . import lex_deps from .fastapi_injector import inject_from_req @@ -36,26 +34,12 @@ def bearer_scheme(authorization=Header(None)): # noqa: ANN201, D103 def get_resource_permissions( # noqa: D103 - resources: ResourceQueries = Depends(lex_deps.get_resource_queries), -) -> LexGetResourcePermissions: - return LexGetResourcePermissions(resources) + resources: ResourceRepository = Depends(lex_deps.get_resource_repository), +) -> ResourcePermissionQueries: + return ResourcePermissionQueries(resources) -def get_is_resource_protected( # noqa: D103 - repo: ResourceQueries = Depends(lex_deps.get_resource_queries), -) -> LexIsResourceProtected: - return LexIsResourceProtected(repo) - - -def get_auth_service( # noqa: D103 - config: JWTAuthServiceConfig = Depends(inject_from_req(JWTAuthServiceConfig)), - query: LexIsResourceProtected = Depends(get_is_resource_protected), -) -> JWTAuthService: - return JWTAuthService( - pubkey_path=config.pubkey_path, - is_resource_protected=query, - ) - +get_auth_service = inject_from_req(JWTAuthService) # TODO this one uses "bearer_scheme" # get_user uses "auth_scheme" diff --git a/karp-backend/src/karp/karp_v6_api/routes/entries_api.py b/karp-backend/src/karp/karp_v6_api/routes/entries_api.py index b1d0793c..1259a7e9 100644 --- a/karp-backend/src/karp/karp_v6_api/routes/entries_api.py +++ b/karp-backend/src/karp/karp_v6_api/routes/entries_api.py @@ -14,7 +14,7 @@ ) from starlette import responses -from karp.auth_infrastructure import JWTAuthService +from karp.auth_infrastructure import ResourcePermissionQueries from karp.entry_commands import EntryCommands from karp.lex_core.value_objects import UniqueId, unique_id from karp.lex_core.value_objects.unique_id import UniqueIdStr @@ -41,10 +41,10 @@ def get_history_for_entry( # noqa: ANN201, D103 entry_id: UniqueIdStr, version: Optional[int] = Query(None), user: auth.User = Security(deps.get_user, scopes=["admin"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), entry_queries: EntryQueries = Depends(deps.get_entry_queries), ): - if not auth_service.authorize(auth.PermissionLevel.admin, user, [resource_id]): + if not resource_permissions.has_permission(auth.PermissionLevel.admin, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", @@ -70,10 +70,10 @@ def add_entry( # noqa: ANN201, D103 resource_id: str, data: schemas.EntryAdd, user: User = Security(deps.get_user, scopes=["write"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), entry_commands: EntryCommands = Depends(inject_from_req(EntryCommands)), ): - if not auth_service.authorize(PermissionLevel.write, user, [resource_id]): + if not resource_permissions.has_permission(PermissionLevel.write, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", @@ -116,10 +116,10 @@ def update_entry( # noqa: ANN201, D103 entry_id: UniqueId, data: schemas.EntryUpdate, user: User = Security(deps.get_user, scopes=["write"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), entry_commands: EntryCommands = Depends(inject_from_req(EntryCommands)), ): - if not auth_service.authorize(PermissionLevel.write, user, [resource_id]): + if not resource_permissions.has_permission(PermissionLevel.write, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403, detail="Not enough permissions", @@ -179,11 +179,11 @@ def delete_entry( # noqa: ANN201 entry_id: UniqueId, version: int, user: User = Security(deps.get_user, scopes=["write"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), entry_commands: EntryCommands = Depends(inject_from_req(EntryCommands)), ): """Delete a entry from a resource.""" - if not auth_service.authorize(PermissionLevel.write, user, [resource_id]): + if not resource_permissions.has_permission(PermissionLevel.write, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", diff --git a/karp-backend/src/karp/karp_v6_api/routes/history_api.py b/karp-backend/src/karp/karp_v6_api/routes/history_api.py index db864a44..7363c5a6 100644 --- a/karp-backend/src/karp/karp_v6_api/routes/history_api.py +++ b/karp-backend/src/karp/karp_v6_api/routes/history_api.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Security, status from karp import auth, lex -from karp.auth_infrastructure import JWTAuthService +from karp.auth_infrastructure import ResourcePermissionQueries from karp.lex_core.value_objects import unique_id from karp.lex_core.value_objects.unique_id import UniqueIdStr @@ -34,10 +34,10 @@ def get_diff( # noqa: ANN201, D103 from_date: Optional[float] = None, to_date: Optional[float] = None, entry: Optional[Dict] = None, - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), entry_queries: EntryQueries = Depends(deps.get_entry_queries), ): - if not auth_service.authorize(auth.PermissionLevel.admin, user, [resource_id]): + if not resource_permissions.has_permission(auth.PermissionLevel.admin, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", @@ -70,10 +70,10 @@ def get_history( # noqa: ANN201, D103 from_version: Optional[int] = Query(None), current_page: int = Query(0), page_size: int = Query(100), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), entry_queries: EntryQueries = Depends(deps.get_entry_queries), ): - if not auth_service.authorize(auth.PermissionLevel.admin, user, [resource_id]): + if not resource_permissions.has_permission(auth.PermissionLevel.admin, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", diff --git a/karp-backend/src/karp/karp_v6_api/routes/query_api.py b/karp-backend/src/karp/karp_v6_api/routes/query_api.py index d535f014..bb358941 100644 --- a/karp-backend/src/karp/karp_v6_api/routes/query_api.py +++ b/karp-backend/src/karp/karp_v6_api/routes/query_api.py @@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Query, Security, status from karp import auth, search -from karp.auth_infrastructure import JWTAuthService +from karp.auth_infrastructure import ResourcePermissionQueries from karp.main import errors as karp_errors from karp.search.application.queries import QueryRequest @@ -37,11 +37,11 @@ def get_entries_by_id( # noqa: ANN201, D103 regex=r"^\w(,\w)*", ), user: auth.User = Security(deps.get_user_optional, scopes=["read"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), search_service: Es6SearchService = Depends(inject_from_req(Es6SearchService)), ): logger.debug("karp_v6_api.views.get_entries_by_id") - if not auth_service.authorize(auth.PermissionLevel.read, user, [resource_id]): + if not resource_permissions.has_permission(auth.PermissionLevel.read, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403, detail="Not enough permissions", @@ -67,12 +67,12 @@ def query_split( # noqa: ANN201, D103 size: int = Query(25, description="Number of entries in page."), lexicon_stats: bool = Query(True, description="Show the hit count per lexicon"), user: auth.User = Security(deps.get_user_optional, scopes=["read"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), search_service: Es6SearchService = Depends(inject_from_req(Es6SearchService)), ): logger.debug("/query/split called", extra={"resources": resources}) resource_list = resources.split(",") - if not auth_service.authorize(auth.PermissionLevel.read, user, resource_list): + if not resource_permissions.has_permission(auth.PermissionLevel.read, user, resource_list): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", @@ -139,7 +139,7 @@ def query( # noqa: ANN201 None, description="Comma-separated list of which fields to remove from result" ), user: auth.User = Security(deps.get_user_optional, scopes=["read"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), search_service: Es6SearchService = Depends(inject_from_req(Es6SearchService)), ): """ @@ -151,7 +151,7 @@ def query( # noqa: ANN201 extra={"resources": resources, "from": from_, "size": size}, ) resource_list = resources.split(",") - if not auth_service.authorize(auth.PermissionLevel.read, user, resource_list): + if not resource_permissions.has_permission(auth.PermissionLevel.read, user, resource_list): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", diff --git a/karp-backend/src/karp/karp_v6_api/routes/resources_api.py b/karp-backend/src/karp/karp_v6_api/routes/resources_api.py index 9bb38473..ec626470 100644 --- a/karp-backend/src/karp/karp_v6_api/routes/resources_api.py +++ b/karp-backend/src/karp/karp_v6_api/routes/resources_api.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from karp.auth.application.queries.resources import ResourcePermissionDto -from karp.auth_infrastructure import LexGetResourcePermissions +from karp.auth_infrastructure import ResourcePermissionQueries from karp.karp_v6_api.schemas import ResourcePublic, ResourceProtected from karp.karp_v6_api import dependencies as deps from karp.karp_v6_api.dependencies.fastapi_injector import inject_from_req @@ -17,9 +17,9 @@ @router.get("/permissions", response_model=list[ResourcePermissionDto]) def list_resource_permissions( # noqa: ANN201, D103 - query: LexGetResourcePermissions = Depends(deps.get_resource_permissions), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), ): - return query.query() + return resource_permissions.get_resource_permissions() @router.get( diff --git a/karp-backend/src/karp/karp_v6_api/routes/stats_api.py b/karp-backend/src/karp/karp_v6_api/routes/stats_api.py index 7d47f060..3dc16d51 100644 --- a/karp-backend/src/karp/karp_v6_api/routes/stats_api.py +++ b/karp-backend/src/karp/karp_v6_api/routes/stats_api.py @@ -11,7 +11,7 @@ ) from karp import auth -from karp.auth_infrastructure import JWTAuthService +from karp.auth_infrastructure import ResourcePermissionQueries from karp.foundation.value_objects import PermissionLevel from karp.search_infrastructure.queries import Es6SearchService from karp.karp_v6_api import schemas # noqa: F401 @@ -36,10 +36,10 @@ def get_field_values( # noqa: ANN201, D103 resource_id: str, field: str, user: auth.User = Security(deps.get_user_optional, scopes=["read"]), - auth_service: JWTAuthService = Depends(deps.get_auth_service), + resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permissions), search_service: Es6SearchService = Depends(inject_from_req(Es6SearchService)), ): - if not auth_service.authorize(PermissionLevel.read, user, [resource_id]): + if not resource_permissions.has_permission(PermissionLevel.read, user, [resource_id]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", diff --git a/tests/integration/test_jwt_auth_service.py b/tests/integration/test_jwt_auth_service.py index c9dcf7cc..bfc966a6 100644 --- a/tests/integration/test_jwt_auth_service.py +++ b/tests/integration/test_jwt_auth_service.py @@ -18,7 +18,6 @@ JWTPayload, # noqa: F401 ) from tests.integration.auth.adapters import create_access_token -from tests.unit.auth import adapters # Generate our key @@ -34,7 +33,6 @@ def jwt_authenticator() -> None: return JWTAuthService( pubkey_path=Path("assets/testing/pubkey.pem"), - is_resource_protected=adapters.InMemoryIsResourceProtected(), ) diff --git a/tests/unit/auth/adapters.py b/tests/unit/auth/adapters.py deleted file mode 100644 index 8f8dcb0a..00000000 --- a/tests/unit/auth/adapters.py +++ /dev/null @@ -1,10 +0,0 @@ -from karp.auth import PermissionLevel -from karp.auth_infrastructure import LexIsResourceProtected - - -class InMemoryIsResourceProtected(LexIsResourceProtected): - def __init__(self): - pass - - def query(self, resource_id: str, level: PermissionLevel) -> bool: - return True