feat: update auth logic

This commit is contained in:
2026-06-09 19:37:32 +03:00
parent e1f074b94d
commit 6c3628edfa
12 changed files with 40 additions and 189 deletions

View File

@@ -7,15 +7,3 @@ class ILegalEntityRepository(ABC):
@abstractmethod @abstractmethod
async def get_by_user_id(self, user_id: str) -> LegalEntityEntity | None: async def get_by_user_id(self, user_id: str) -> LegalEntityEntity | None:
raise NotImplementedError 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

View File

@@ -1,7 +1,4 @@
from src.application.commands.organization_commands import ( from src.application.commands.organization_commands import GetOrganizationCommand
GetOrganizationCommand,
ListOrganizationsCommand,
)
from src.application.commands.organization_wallet_commands import ( from src.application.commands.organization_wallet_commands import (
GetOrganizationMnemonicCommand, GetOrganizationMnemonicCommand,
GetOrganizationSecretKeysCommand, GetOrganizationSecretKeysCommand,

View File

@@ -1,28 +1,17 @@
from __future__ import annotations from __future__ import annotations
from src.application.abstractions import IUnitOfWork 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.contracts import ILogger
from src.application.domain.entities.legal_entity import LegalEntityEntity from src.application.domain.entities.legal_entity import LegalEntityEntity
from src.infrastructure.database.decorators import transactional 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: class GetOrganizationCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work self._unit_of_work = unit_of_work
self._logger = logger self._logger = logger
@transactional @transactional
async def __call__(self, organization_id: str) -> LegalEntityEntity: async def __call__(self, user_id: str) -> LegalEntityEntity:
return await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) return await require_legal_entity(user_id, self._unit_of_work)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from src.application.abstractions import IUnitOfWork 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.contracts import ILogger
from src.application.domain.entities.legal_entity import LegalEntityEntity from src.application.domain.entities.legal_entity import LegalEntityEntity
from src.application.domain.entities.organization_wallet import OrganizationWalletEntity from src.application.domain.entities.organization_wallet import OrganizationWalletEntity
@@ -20,9 +21,9 @@ class ListOrganizationWalletsCommand:
self._logger = logger self._logger = logger
@transactional @transactional
async def __call__(self, organization_id: str) -> list[OrganizationWalletEntity]: async def __call__(self, user_id: str) -> list[OrganizationWalletEntity]:
await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) org = await require_legal_entity(user_id, self._unit_of_work)
return await self._unit_of_work.organization_wallet_repository.list_by_organization(organization_id) return await self._unit_of_work.organization_wallet_repository.list_by_organization(org.id)
class GetOrganizationMnemonicCommand: class GetOrganizationMnemonicCommand:
@@ -31,14 +32,14 @@ class GetOrganizationMnemonicCommand:
self._logger = logger self._logger = logger
@transactional @transactional
async def __call__(self, organization_id: str) -> str: async def __call__(self, user_id: str) -> str:
org = await self._require_wallets_org(organization_id) org = await self._require_wallets_org(user_id)
if not is_crypto_ready(): if not is_crypto_ready():
raise ServiceUnavailableException(message='Crypto service not ready') raise ServiceUnavailableException(message='Crypto service not ready')
return decrypt_mnemonic(org.encrypted_mnemonic or '') return decrypt_mnemonic(org.encrypted_mnemonic or '')
async def _require_wallets_org(self, organization_id: str) -> LegalEntityEntity: async def _require_wallets_org(self, user_id: str) -> LegalEntityEntity:
org = await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) org = await require_legal_entity(user_id, self._unit_of_work)
if not org.encrypted_mnemonic: if not org.encrypted_mnemonic:
raise NotFoundException(message='Wallets not created for organization') raise NotFoundException(message='Wallets not created for organization')
return org return org
@@ -50,15 +51,15 @@ class GetOrganizationSecretKeysCommand:
self._logger = logger self._logger = logger
@transactional @transactional
async def __call__(self, organization_id: str) -> list[DerivedSecretKey]: async def __call__(self, user_id: str) -> list[DerivedSecretKey]:
org = await self._require_wallets_org(organization_id) org = await self._require_wallets_org(user_id)
if not is_crypto_ready(): if not is_crypto_ready():
raise ServiceUnavailableException(message='Crypto service not ready') raise ServiceUnavailableException(message='Crypto service not ready')
mnemonic = decrypt_mnemonic(org.encrypted_mnemonic or '') mnemonic = decrypt_mnemonic(org.encrypted_mnemonic or '')
return derive_all_private_keys(mnemonic) return derive_all_private_keys(mnemonic)
async def _require_wallets_org(self, organization_id: str) -> LegalEntityEntity: async def _require_wallets_org(self, user_id: str) -> LegalEntityEntity:
org = await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) org = await require_legal_entity(user_id, self._unit_of_work)
if not org.encrypted_mnemonic: if not org.encrypted_mnemonic:
raise NotFoundException(message='Wallets not created for organization') raise NotFoundException(message='Wallets not created for organization')
return org return org

View File

@@ -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 from src.application.domain.dto.keys import JwtPublicKey, JwtPublicKeySet

View File

@@ -4,8 +4,7 @@ from pydantic import BaseModel
class AccessTokenPayload(BaseModel): class AccessTokenPayload(BaseModel):
sub: str sub: str
type: str type: str
sid: str | None = None sid: str
role: str | None = None
iat: int iat: int
nbf: int nbf: int
exp: int exp: int
@@ -17,8 +16,3 @@ class AuthContext(BaseModel):
user_id: str user_id: str
sid: str sid: str
token: AccessTokenPayload token: AccessTokenPayload
class AdminAuthContext(BaseModel):
admin_user_id: str
role: str

View File

@@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from sqlalchemy import func, select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository
from src.application.contracts import ILogger from src.application.contracts import ILogger
from src.application.domain.entities.legal_entity import LegalEntityEntity 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 from src.infrastructure.database.models.legal_entity import LegalEntityModel
@@ -52,39 +52,3 @@ class LegalEntityRepository(ILegalEntityRepository):
self._logger.exception(str(exc)) self._logger.exception(str(exc))
raise InternalException(message=f'Database error: {exc}') from 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

View File

@@ -23,8 +23,7 @@ class JwtService(IJwtService):
return AccessTokenPayload( return AccessTokenPayload(
sub=str(payload['sub']), sub=str(payload['sub']),
type='access', type='access',
sid=str(payload['sid']) if payload.get('sid') else None, sid=str(payload['sid']),
role=str(payload['role']) if payload.get('role') else None,
iat=int(payload['iat']), iat=int(payload['iat']),
nbf=int(payload['nbf']), nbf=int(payload['nbf']),
exp=int(payload['exp']), exp=int(payload['exp']),
@@ -84,9 +83,9 @@ class JwtService(IJwtService):
options=options, options=options,
) )
if 'sid' not in payload and 'role' not in payload: if 'sid' not in payload:
self._logger.warning(f'JWT missing sid or role claim kid={kid}') self._logger.warning(f'JWT missing sid claim kid={kid}')
raise ApplicationException(status_code=401, message='Missing token claim: sid or role') raise ApplicationException(status_code=401, message='Missing token claim: sid')
if 'type' not in payload: if 'type' not in payload:
self._logger.warning(f'JWT missing type claim kid={kid}') self._logger.warning(f'JWT missing type claim kid={kid}')

View File

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

View File

@@ -7,7 +7,6 @@ from src.application.commands import (
GetOrganizationSecretKeysCommand, GetOrganizationSecretKeysCommand,
GetPurchaseRequestCommand, GetPurchaseRequestCommand,
ListOrganizationWalletsCommand, ListOrganizationWalletsCommand,
ListOrganizationsCommand,
ListPurchaseRequestsCommand, ListPurchaseRequestsCommand,
) )
from src.application.contracts import ILogger 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) 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( def get_get_organization_command(
logger: ILogger = Depends(get_logger), logger: ILogger = Depends(get_logger),
unit_of_work: IUnitOfWork = Depends(get_unit_of_work), unit_of_work: IUnitOfWork = Depends(get_unit_of_work),

View File

@@ -1,24 +1,21 @@
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends
from src.application.commands import ( from src.application.commands import (
GetOrganizationCommand, GetOrganizationCommand,
GetOrganizationMnemonicCommand, GetOrganizationMnemonicCommand,
GetOrganizationSecretKeysCommand, GetOrganizationSecretKeysCommand,
ListOrganizationWalletsCommand, ListOrganizationWalletsCommand,
ListOrganizationsCommand,
) )
from src.application.domain.dto import AdminAuthContext from src.application.domain.dto import AuthContext
from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role from src.presentation.decorators.auth import require_access_token
from src.presentation.dependencies.commands import ( from src.presentation.dependencies.commands import (
get_get_organization_command, get_get_organization_command,
get_get_organization_mnemonic_command, get_get_organization_mnemonic_command,
get_get_organization_secret_keys_command, get_get_organization_secret_keys_command,
get_list_organization_wallets_command, get_list_organization_wallets_command,
get_list_organizations_command,
) )
from src.presentation.schemas.organization import ( from src.presentation.schemas.organization import (
MnemonicResponse, MnemonicResponse,
OrganizationListResponse,
OrganizationResponse, OrganizationResponse,
SecretKeyResponse, SecretKeyResponse,
WalletResponse, WalletResponse,
@@ -33,57 +30,39 @@ from src.presentation.serializers.organization import (
organizations_router = APIRouter(prefix='/organizations', tags=['organizations']) organizations_router = APIRouter(prefix='/organizations', tags=['organizations'])
@organizations_router.get('', response_model=OrganizationListResponse) @organizations_router.get('', response_model=OrganizationResponse)
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)
async def get_organization( async def get_organization(
organization_id: str, auth: AuthContext = Depends(require_access_token),
auth: AdminAuthContext = Depends(require_admin_access),
command: GetOrganizationCommand = Depends(get_get_organization_command), command: GetOrganizationCommand = Depends(get_get_organization_command),
): ):
org = await command(organization_id) org = await command(auth.user_id)
return organization_to_response(org) 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( async def list_organization_wallets(
organization_id: str, auth: AuthContext = Depends(require_access_token),
auth: AdminAuthContext = Depends(require_admin_access),
command: ListOrganizationWalletsCommand = Depends(get_list_organization_wallets_command), 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] 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( async def get_organization_mnemonic(
organization_id: str, auth: AuthContext = Depends(require_access_token),
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
command: GetOrganizationMnemonicCommand = Depends(get_get_organization_mnemonic_command), command: GetOrganizationMnemonicCommand = Depends(get_get_organization_mnemonic_command),
): ):
mnemonic = await command(organization_id) mnemonic = await command(auth.user_id)
return mnemonic_to_response(mnemonic) 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( async def get_organization_secret_keys(
organization_id: str, auth: AuthContext = Depends(require_access_token),
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
command: GetOrganizationSecretKeysCommand = Depends(get_get_organization_secret_keys_command), command: GetOrganizationSecretKeysCommand = Depends(get_get_organization_secret_keys_command),
): ):
keys = await command(organization_id) keys = await command(auth.user_id)
return [ return [
secret_key_to_response( secret_key_to_response(
chain=k.chain, chain=k.chain,

View File

@@ -27,11 +27,6 @@ class OrganizationResponse(BaseModel):
updated_at: str | None updated_at: str | None
class OrganizationListResponse(BaseModel):
items: list[OrganizationResponse]
total: int
class WalletResponse(BaseModel): class WalletResponse(BaseModel):
id: str id: str
chain: str chain: str