feat: update auth logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -17,8 +16,3 @@ class AuthContext(BaseModel):
|
||||
user_id: str
|
||||
sid: str
|
||||
token: AccessTokenPayload
|
||||
|
||||
|
||||
class AdminAuthContext(BaseModel):
|
||||
admin_user_id: str
|
||||
role: str
|
||||
@@ -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
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user