feat: add portfolio ownership model, auth guards, and migration
This commit is contained in:
+66
-16
@@ -2,7 +2,7 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
|
||||
from app.core.auth import Permission, authorize_request
|
||||
from app.core.auth import AuthContext, Permission, authorize_request
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.schemas.portfolio import (
|
||||
@@ -41,6 +41,28 @@ def _close_repository(repository: object) -> None:
|
||||
repository.close()
|
||||
|
||||
|
||||
def _ownership_required(settings: Settings) -> bool:
|
||||
return settings.db_enabled
|
||||
|
||||
|
||||
def _require_owner_context(auth_context: AuthContext) -> str:
|
||||
if auth_context.user_id is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ownership context is required")
|
||||
return auth_context.user_id
|
||||
|
||||
|
||||
def _ensure_portfolio_owned(repository: object, portfolio_id: str, auth_context: AuthContext) -> None:
|
||||
if auth_context.permission == Permission.ADMIN:
|
||||
return
|
||||
owner_user_id = _require_owner_context(auth_context)
|
||||
try:
|
||||
portfolio = repository.get(portfolio_id)
|
||||
except ValueError as exc:
|
||||
_raise_portfolio_error(exc)
|
||||
if portfolio.owner_user_id != owner_user_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Portfolio ownership mismatch")
|
||||
|
||||
|
||||
@router.get("", response_model=list[PortfolioListItem])
|
||||
def list_portfolios(
|
||||
request: Request,
|
||||
@@ -48,10 +70,13 @@ def list_portfolios(
|
||||
status_filter: str | None = None,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> list[PortfolioListItem]:
|
||||
authorize_request(Permission.READ, request, settings)
|
||||
auth_context = authorize_request(Permission.READ, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.list_portfolios(broker=broker, status=status_filter)
|
||||
owner_user_id: str | None = None
|
||||
if _ownership_required(settings) and auth_context.permission != Permission.ADMIN:
|
||||
owner_user_id = _require_owner_context(auth_context)
|
||||
return service.list_portfolios(broker=broker, status=status_filter, owner_user_id=owner_user_id)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
@@ -64,9 +89,11 @@ def get_portfolio(
|
||||
recent_trades: int = 5,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.READ, request, settings)
|
||||
auth_context = authorize_request(Permission.READ, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.get_portfolio_status(portfolio_id=portfolio_id, recent_trades=recent_trades)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -83,10 +110,13 @@ def create_portfolio(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.create_portfolio(payload)
|
||||
owner_user_id = auth_context.user_id or "system"
|
||||
if _ownership_required(settings):
|
||||
owner_user_id = _require_owner_context(auth_context)
|
||||
return service.create_portfolio(payload, owner_user_id=owner_user_id)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
@@ -99,9 +129,11 @@ def update_portfolio(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.update_portfolio(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -115,9 +147,11 @@ def buy_portfolio_asset(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.buy(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -131,9 +165,11 @@ def sell_portfolio_asset(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.sell(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -147,9 +183,11 @@ def invest_buffer(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.invest(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -163,9 +201,11 @@ def transfer_buffer(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.transfer(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -179,9 +219,11 @@ def reset_portfolio(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.reset(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
@@ -216,9 +258,11 @@ def create_comment(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> CommentResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.create_comment(portfolio_id, payload)
|
||||
except ValueError as exc:
|
||||
_raise_portfolio_error(exc)
|
||||
@@ -233,9 +277,11 @@ def list_comments(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> list[CommentResponse]:
|
||||
authorize_request(Permission.READ, request, settings)
|
||||
auth_context = authorize_request(Permission.READ, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.list_comments(portfolio_id)
|
||||
except ValueError as exc:
|
||||
_raise_portfolio_error(exc)
|
||||
@@ -252,9 +298,11 @@ def update_comment(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> CommentResponse:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.update_comment(portfolio_id, comment_id, payload)
|
||||
except ValueError as exc:
|
||||
_raise_portfolio_error(exc)
|
||||
@@ -270,9 +318,11 @@ def delete_comment(
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> Response:
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
service.delete_comment(portfolio_id, comment_id)
|
||||
except ValueError as exc:
|
||||
_raise_portfolio_error(exc)
|
||||
|
||||
Reference in New Issue
Block a user