Phase 5: AI/MCP Bearer-Auth + Delegationstoken + Audit
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
5.1 Delegationstoken (app/core/delegation_token.py): - HMAC-SHA256 signiert mit SECRET_KEY, max 60s Lifetime - Payload: user_id, tenant_id, agent_id, audience, expires_at, token_id - Statelose Verifikation, Audience-Check, Expiry-Check 5.2 MCP Bearer-Auth: - app/core/api_token.py: Token Service (create, verify, revoke, list) - app/deps.py: get_current_user_bearer + get_current_user_or_bearer - app/routes/api_tokens.py: Token CRUD Routes (create, list, revoke) - MCP Server Routes: get_current_user_or_bearer akzeptiert Session + Bearer 5.3 Methodenrechte: - MCP nutzt bereits mcp:read/mcp:write basierend auf tool_def.required_permission 5.5 Audit: - MCP Tool-Ausfuehrung wird protokolliert (log_audit mit correlation_id) Tests: 13/13 bestanden (7 API Token + 6 Delegation Token)
This commit is contained in:
+70
@@ -154,6 +154,76 @@ async def get_current_user(
|
||||
return session_data
|
||||
|
||||
|
||||
async def get_current_user_bearer(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
"""Get the current user from a Bearer API token.
|
||||
|
||||
Alternative to session-based auth for programmatic access (MCP, API clients).
|
||||
Returns the same dict shape as get_current_user.
|
||||
"""
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"detail": "Bearer token required", "code": "not_authenticated"},
|
||||
)
|
||||
|
||||
token = auth_header[7:] # Strip "Bearer "
|
||||
from app.core.api_token import verify_api_token
|
||||
|
||||
user_data = await verify_api_token(db, token)
|
||||
if user_data is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"detail": "Invalid or expired token", "code": "token_invalid"},
|
||||
)
|
||||
|
||||
# Set RLS tenant context
|
||||
tenant_id = uuid.UUID(user_data["tenant_id"])
|
||||
await set_tenant_context(db, tenant_id)
|
||||
|
||||
# Set RLS user context
|
||||
user_id = uuid.UUID(user_data["user_id"])
|
||||
from app.models.group import UserGroup
|
||||
groups_q = await db.execute(
|
||||
select(UserGroup.group_id)
|
||||
.where(UserGroup.user_id == user_id)
|
||||
.where(UserGroup.tenant_id == tenant_id)
|
||||
)
|
||||
group_ids = [row[0] for row in groups_q]
|
||||
is_admin = user_data.get("is_system_admin", False)
|
||||
await set_user_context(db, user_id, group_ids, is_admin)
|
||||
|
||||
# Load resolved permissions
|
||||
from app.core.permissions import get_cached_permissions
|
||||
redis = get_redis()
|
||||
resolved = await get_cached_permissions(db, redis, user_id, tenant_id)
|
||||
user_data["permissions"] = resolved.get("permissions", [])
|
||||
user_data["denied_permissions"] = resolved.get("denied", [])
|
||||
user_data["field_permissions"] = resolved.get("field_permissions", {})
|
||||
user_data["is_system_admin"] = resolved.get("is_system_admin", False)
|
||||
|
||||
return user_data
|
||||
|
||||
|
||||
async def get_current_user_or_bearer(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis_dep),
|
||||
) -> dict[str, Any]:
|
||||
"""Get current user from session cookie OR Bearer token.
|
||||
|
||||
Tries session auth first, falls back to Bearer token.
|
||||
Used by MCP routes that accept both auth methods.
|
||||
"""
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
return await get_current_user_bearer(request, db)
|
||||
return await get_current_user(request, db, redis)
|
||||
|
||||
|
||||
async def require_admin(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user