diff --git a/src/application/abstractions/repositories/i_legal_entity_repository.py b/src/application/abstractions/repositories/i_legal_entity_repository.py index 7522dc2..bd3ab48 100644 --- a/src/application/abstractions/repositories/i_legal_entity_repository.py +++ b/src/application/abstractions/repositories/i_legal_entity_repository.py @@ -7,15 +7,3 @@ class ILegalEntityRepository(ABC): @abstractmethod async def get_by_user_id(self, user_id: str) -> LegalEntityEntity | None: raise NotImplementedError - - @abstractmethod - async def get_by_id(self, organization_id: str) -> LegalEntityEntity: - raise NotImplementedError - - @abstractmethod - async def list_all(self, *, limit: int, offset: int) -> list[LegalEntityEntity]: - raise NotImplementedError - - @abstractmethod - async def count_all(self) -> int: - raise NotImplementedError diff --git a/src/application/commands/__init__.py b/src/application/commands/__init__.py index 5ca2461..01f0c16 100644 --- a/src/application/commands/__init__.py +++ b/src/application/commands/__init__.py @@ -1,7 +1,4 @@ -from src.application.commands.organization_commands import ( - GetOrganizationCommand, - ListOrganizationsCommand, -) +from src.application.commands.organization_commands import GetOrganizationCommand from src.application.commands.organization_wallet_commands import ( GetOrganizationMnemonicCommand, GetOrganizationSecretKeysCommand, diff --git a/src/application/commands/organization_commands.py b/src/application/commands/organization_commands.py index 1330778..b05774f 100644 --- a/src/application/commands/organization_commands.py +++ b/src/application/commands/organization_commands.py @@ -1,28 +1,17 @@ from __future__ import annotations from src.application.abstractions import IUnitOfWork +from src.application.commands.legal_entity_guard import require_legal_entity from src.application.contracts import ILogger from src.application.domain.entities.legal_entity import LegalEntityEntity from src.infrastructure.database.decorators import transactional -class ListOrganizationsCommand: - def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): - self._unit_of_work = unit_of_work - self._logger = logger - - @transactional - async def __call__(self, *, limit: int = 50, offset: int = 0) -> tuple[list[LegalEntityEntity], int]: - items = await self._unit_of_work.legal_entity_repository.list_all(limit=limit, offset=offset) - total = await self._unit_of_work.legal_entity_repository.count_all() - return items, total - - class GetOrganizationCommand: def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): self._unit_of_work = unit_of_work self._logger = logger @transactional - async def __call__(self, organization_id: str) -> LegalEntityEntity: - return await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) + async def __call__(self, user_id: str) -> LegalEntityEntity: + return await require_legal_entity(user_id, self._unit_of_work) diff --git a/src/application/commands/organization_wallet_commands.py b/src/application/commands/organization_wallet_commands.py index f74e4f2..cc3bb68 100644 --- a/src/application/commands/organization_wallet_commands.py +++ b/src/application/commands/organization_wallet_commands.py @@ -1,6 +1,7 @@ from __future__ import annotations from src.application.abstractions import IUnitOfWork +from src.application.commands.legal_entity_guard import require_legal_entity from src.application.contracts import ILogger from src.application.domain.entities.legal_entity import LegalEntityEntity from src.application.domain.entities.organization_wallet import OrganizationWalletEntity @@ -20,9 +21,9 @@ class ListOrganizationWalletsCommand: self._logger = logger @transactional - async def __call__(self, organization_id: str) -> list[OrganizationWalletEntity]: - await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) - return await self._unit_of_work.organization_wallet_repository.list_by_organization(organization_id) + async def __call__(self, user_id: str) -> list[OrganizationWalletEntity]: + org = await require_legal_entity(user_id, self._unit_of_work) + return await self._unit_of_work.organization_wallet_repository.list_by_organization(org.id) class GetOrganizationMnemonicCommand: @@ -31,14 +32,14 @@ class GetOrganizationMnemonicCommand: self._logger = logger @transactional - async def __call__(self, organization_id: str) -> str: - org = await self._require_wallets_org(organization_id) + async def __call__(self, user_id: str) -> str: + org = await self._require_wallets_org(user_id) if not is_crypto_ready(): raise ServiceUnavailableException(message='Crypto service not ready') return decrypt_mnemonic(org.encrypted_mnemonic or '') - async def _require_wallets_org(self, organization_id: str) -> LegalEntityEntity: - org = await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) + async def _require_wallets_org(self, user_id: str) -> LegalEntityEntity: + org = await require_legal_entity(user_id, self._unit_of_work) if not org.encrypted_mnemonic: raise NotFoundException(message='Wallets not created for organization') return org @@ -50,15 +51,15 @@ class GetOrganizationSecretKeysCommand: self._logger = logger @transactional - async def __call__(self, organization_id: str) -> list[DerivedSecretKey]: - org = await self._require_wallets_org(organization_id) + async def __call__(self, user_id: str) -> list[DerivedSecretKey]: + org = await self._require_wallets_org(user_id) if not is_crypto_ready(): raise ServiceUnavailableException(message='Crypto service not ready') mnemonic = decrypt_mnemonic(org.encrypted_mnemonic or '') return derive_all_private_keys(mnemonic) - async def _require_wallets_org(self, organization_id: str) -> LegalEntityEntity: - org = await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) + async def _require_wallets_org(self, user_id: str) -> LegalEntityEntity: + org = await require_legal_entity(user_id, self._unit_of_work) if not org.encrypted_mnemonic: raise NotFoundException(message='Wallets not created for organization') return org diff --git a/src/application/domain/dto/__init__.py b/src/application/domain/dto/__init__.py index 5180d14..fbf3570 100644 --- a/src/application/domain/dto/__init__.py +++ b/src/application/domain/dto/__init__.py @@ -1,2 +1,2 @@ -from src.application.domain.dto.token import AccessTokenPayload, AdminAuthContext, AuthContext +from src.application.domain.dto.token import AccessTokenPayload, AuthContext from src.application.domain.dto.keys import JwtPublicKey, JwtPublicKeySet \ No newline at end of file diff --git a/src/application/domain/dto/token.py b/src/application/domain/dto/token.py index e6a9e00..f46391c 100644 --- a/src/application/domain/dto/token.py +++ b/src/application/domain/dto/token.py @@ -4,8 +4,7 @@ from pydantic import BaseModel class AccessTokenPayload(BaseModel): sub: str type: str - sid: str | None = None - role: str | None = None + sid: str iat: int nbf: int exp: int @@ -16,9 +15,4 @@ class AccessTokenPayload(BaseModel): class AuthContext(BaseModel): user_id: str sid: str - token: AccessTokenPayload - - -class AdminAuthContext(BaseModel): - admin_user_id: str - role: str \ No newline at end of file + token: AccessTokenPayload \ No newline at end of file diff --git a/src/infrastructure/database/repositories/legal_entity_repository.py b/src/infrastructure/database/repositories/legal_entity_repository.py index 1c157d2..e0e4907 100644 --- a/src/infrastructure/database/repositories/legal_entity_repository.py +++ b/src/infrastructure/database/repositories/legal_entity_repository.py @@ -1,13 +1,13 @@ from __future__ import annotations -from sqlalchemy import func, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import SQLAlchemyError from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository from src.application.contracts import ILogger from src.application.domain.entities.legal_entity import LegalEntityEntity -from src.application.domain.exceptions import InternalException, NotFoundException +from src.application.domain.exceptions import InternalException from src.infrastructure.database.models.legal_entity import LegalEntityModel @@ -52,39 +52,3 @@ class LegalEntityRepository(ILegalEntityRepository): self._logger.exception(str(exc)) raise InternalException(message=f'Database error: {exc}') from exc - async def get_by_id(self, organization_id: str) -> LegalEntityEntity: - try: - stmt = select(LegalEntityModel).where(LegalEntityModel.id == organization_id) - result = await self._session.execute(stmt) - model = result.scalar_one_or_none() - if model is None: - raise NotFoundException(message='Organization not found') - return self._to_entity(model) - except NotFoundException: - raise - except SQLAlchemyError as exc: - self._logger.exception(str(exc)) - raise InternalException(message=f'Database error: {exc}') from exc - - async def list_all(self, *, limit: int, offset: int) -> list[LegalEntityEntity]: - try: - stmt = ( - select(LegalEntityModel) - .order_by(LegalEntityModel.created_at.desc()) - .limit(limit) - .offset(offset) - ) - result = await self._session.execute(stmt) - return [self._to_entity(m) for m in result.scalars().all()] - except SQLAlchemyError as exc: - self._logger.exception(str(exc)) - raise InternalException(message=f'Database error: {exc}') from exc - - async def count_all(self) -> int: - try: - stmt = select(func.count()).select_from(LegalEntityModel) - result = await self._session.execute(stmt) - return int(result.scalar_one()) - except SQLAlchemyError as exc: - self._logger.exception(str(exc)) - raise InternalException(message=f'Database error: {exc}') from exc diff --git a/src/infrastructure/security/jwt.py b/src/infrastructure/security/jwt.py index 90cf04d..4274902 100644 --- a/src/infrastructure/security/jwt.py +++ b/src/infrastructure/security/jwt.py @@ -23,8 +23,7 @@ class JwtService(IJwtService): return AccessTokenPayload( sub=str(payload['sub']), type='access', - sid=str(payload['sid']) if payload.get('sid') else None, - role=str(payload['role']) if payload.get('role') else None, + sid=str(payload['sid']), iat=int(payload['iat']), nbf=int(payload['nbf']), exp=int(payload['exp']), @@ -84,9 +83,9 @@ class JwtService(IJwtService): options=options, ) - if 'sid' not in payload and 'role' not in payload: - self._logger.warning(f'JWT missing sid or role claim kid={kid}') - raise ApplicationException(status_code=401, message='Missing token claim: sid or role') + if 'sid' not in payload: + self._logger.warning(f'JWT missing sid claim kid={kid}') + raise ApplicationException(status_code=401, message='Missing token claim: sid') if 'type' not in payload: self._logger.warning(f'JWT missing type claim kid={kid}') diff --git a/src/presentation/decorators/admin_auth.py b/src/presentation/decorators/admin_auth.py deleted file mode 100644 index 28033e1..0000000 --- a/src/presentation/decorators/admin_auth.py +++ /dev/null @@ -1,47 +0,0 @@ -from fastapi import Depends, Request -from fastapi.security.utils import get_authorization_scheme_param - -from src.application.contracts import IJwtService -from src.application.domain.dto import AdminAuthContext -from src.application.domain.exceptions import ForbiddenException, UnauthorizedException -from src.presentation.dependencies.security import get_jwt_service - - -def _extract_bearer_token(request: Request) -> str | None: - auth = request.headers.get('Authorization') - if not auth: - return None - scheme, param = get_authorization_scheme_param(auth) - if scheme.lower() == 'bearer' and param: - return param - return None - - -async def require_admin_access( - request: Request, - jwt_service: IJwtService = Depends(get_jwt_service), -) -> AdminAuthContext: - token = _extract_bearer_token(request) - if not token: - raise UnauthorizedException(message='Authorization Bearer token required') - - payload = await jwt_service.decode_access_token(token) - if payload.type != 'access': - raise UnauthorizedException(message='Invalid token type') - - role = payload.role - if not role: - raise UnauthorizedException(message='Token missing role') - - return AdminAuthContext(admin_user_id=payload.sub, role=role) - - -def require_admin_role(*allowed_roles: str): - allowed = set(allowed_roles) - - async def dependency(auth: AdminAuthContext = Depends(require_admin_access)) -> AdminAuthContext: - if auth.role not in allowed: - raise ForbiddenException(message='Insufficient permissions') - return auth - - return dependency diff --git a/src/presentation/dependencies/commands.py b/src/presentation/dependencies/commands.py index 891a111..29a9396 100644 --- a/src/presentation/dependencies/commands.py +++ b/src/presentation/dependencies/commands.py @@ -7,7 +7,6 @@ from src.application.commands import ( GetOrganizationSecretKeysCommand, GetPurchaseRequestCommand, ListOrganizationWalletsCommand, - ListOrganizationsCommand, ListPurchaseRequestsCommand, ) from src.application.contracts import ILogger @@ -36,13 +35,6 @@ def get_get_purchase_request_command( return GetPurchaseRequestCommand(unit_of_work=unit_of_work, logger=logger) -def get_list_organizations_command( - logger: ILogger = Depends(get_logger), - unit_of_work: IUnitOfWork = Depends(get_unit_of_work), -) -> ListOrganizationsCommand: - return ListOrganizationsCommand(unit_of_work=unit_of_work, logger=logger) - - def get_get_organization_command( logger: ILogger = Depends(get_logger), unit_of_work: IUnitOfWork = Depends(get_unit_of_work), diff --git a/src/presentation/routing/organizations.py b/src/presentation/routing/organizations.py index 0af5cbe..fa7cf02 100644 --- a/src/presentation/routing/organizations.py +++ b/src/presentation/routing/organizations.py @@ -1,24 +1,21 @@ -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends from src.application.commands import ( GetOrganizationCommand, GetOrganizationMnemonicCommand, GetOrganizationSecretKeysCommand, ListOrganizationWalletsCommand, - ListOrganizationsCommand, ) -from src.application.domain.dto import AdminAuthContext -from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role +from src.application.domain.dto import AuthContext +from src.presentation.decorators.auth import require_access_token from src.presentation.dependencies.commands import ( get_get_organization_command, get_get_organization_mnemonic_command, get_get_organization_secret_keys_command, get_list_organization_wallets_command, - get_list_organizations_command, ) from src.presentation.schemas.organization import ( MnemonicResponse, - OrganizationListResponse, OrganizationResponse, SecretKeyResponse, WalletResponse, @@ -33,57 +30,39 @@ from src.presentation.serializers.organization import ( organizations_router = APIRouter(prefix='/organizations', tags=['organizations']) -@organizations_router.get('', response_model=OrganizationListResponse) -async def list_organizations( - limit: int = Query(default=50, ge=1, le=200), - offset: int = Query(default=0, ge=0), - auth: AdminAuthContext = Depends(require_admin_access), - command: ListOrganizationsCommand = Depends(get_list_organizations_command), -): - items, total = await command(limit=limit, offset=offset) - return OrganizationListResponse( - items=[organization_to_response(x) for x in items], - total=total, - ) - - -@organizations_router.get('/{organization_id}', response_model=OrganizationResponse) +@organizations_router.get('', response_model=OrganizationResponse) async def get_organization( - organization_id: str, - auth: AdminAuthContext = Depends(require_admin_access), + auth: AuthContext = Depends(require_access_token), command: GetOrganizationCommand = Depends(get_get_organization_command), ): - org = await command(organization_id) + org = await command(auth.user_id) return organization_to_response(org) -@organizations_router.get('/{organization_id}/wallets', response_model=list[WalletResponse]) +@organizations_router.get('/wallets', response_model=list[WalletResponse]) async def list_organization_wallets( - organization_id: str, - auth: AdminAuthContext = Depends(require_admin_access), + auth: AuthContext = Depends(require_access_token), command: ListOrganizationWalletsCommand = Depends(get_list_organization_wallets_command), ): - wallets = await command(organization_id) + wallets = await command(auth.user_id) return [wallet_to_response(w) for w in wallets] -@organizations_router.get('/{organization_id}/wallets/mnemonic', response_model=MnemonicResponse) +@organizations_router.get('/wallets/mnemonic', response_model=MnemonicResponse) async def get_organization_mnemonic( - organization_id: str, - auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')), + auth: AuthContext = Depends(require_access_token), command: GetOrganizationMnemonicCommand = Depends(get_get_organization_mnemonic_command), ): - mnemonic = await command(organization_id) + mnemonic = await command(auth.user_id) return mnemonic_to_response(mnemonic) -@organizations_router.get('/{organization_id}/wallets/secret-keys', response_model=list[SecretKeyResponse]) +@organizations_router.get('/wallets/secret-keys', response_model=list[SecretKeyResponse]) async def get_organization_secret_keys( - organization_id: str, - auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')), + auth: AuthContext = Depends(require_access_token), command: GetOrganizationSecretKeysCommand = Depends(get_get_organization_secret_keys_command), ): - keys = await command(organization_id) + keys = await command(auth.user_id) return [ secret_key_to_response( chain=k.chain, diff --git a/src/presentation/schemas/organization.py b/src/presentation/schemas/organization.py index f9e8e4f..f70b3bc 100644 --- a/src/presentation/schemas/organization.py +++ b/src/presentation/schemas/organization.py @@ -27,11 +27,6 @@ class OrganizationResponse(BaseModel): updated_at: str | None -class OrganizationListResponse(BaseModel): - items: list[OrganizationResponse] - total: int - - class WalletResponse(BaseModel): id: str chain: str