feat: add authentication and user management with Authentik integration
Build and Push / build (release) Successful in 1m16s
Build and Push / build (release) Successful in 1m16s
- Add PostgreSQL database with async SQLAlchemy - Add Alembic migrations for schema management - Add User, Role, UserPreferences, ApiKey models - Add auth endpoints: /auth/me, /auth/users, /auth/users/sync-from-authentik - Add token validation via Authentik userinfo endpoint - Add bulk user sync from Authentik admin API - Add database health check to diagnostics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
cdb6344013
commit
4f45f9bf37
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Authentication Controller
|
||||
|
||||
Provides authentication endpoints for OIDC token sync and user management.
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.controllers.base import BaseController
|
||||
from src.logging_config import get_logger
|
||||
from src.db import get_async_session
|
||||
from src.auth.schemas import AuthSyncRequest, AuthSyncResponse, UsersListResponse, BulkSyncResultSchema
|
||||
from src.auth.service import AuthService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AuthController(BaseController):
|
||||
"""
|
||||
Controller for authentication operations
|
||||
|
||||
Provides endpoints for:
|
||||
- Token synchronization (login)
|
||||
- User profile retrieval
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
def create_router(self) -> APIRouter:
|
||||
"""Create and configure the router"""
|
||||
router = APIRouter(prefix=self.prefix, tags=self.tags)
|
||||
|
||||
@router.post(
|
||||
"/sync",
|
||||
summary="Sync user from OIDC token",
|
||||
response_model=AuthSyncResponse,
|
||||
responses={
|
||||
200: {"description": "User synced successfully"},
|
||||
401: {"description": "Invalid or expired token"},
|
||||
503: {"description": "Authentication service unavailable"},
|
||||
},
|
||||
)
|
||||
async def sync_user(
|
||||
request: AuthSyncRequest,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
) -> AuthSyncResponse:
|
||||
"""
|
||||
Synchronize user from OIDC access token
|
||||
|
||||
This endpoint should be called after the client obtains an access token
|
||||
from Authentik. It:
|
||||
1. Validates the token via Authentik's userinfo endpoint
|
||||
2. Creates or updates the user in the database
|
||||
3. Syncs roles from Authentik groups
|
||||
4. Returns the user profile with roles and preferences
|
||||
|
||||
The client should store the returned user info for local use.
|
||||
"""
|
||||
service = AuthService(session)
|
||||
|
||||
try:
|
||||
# Validate token with Authentik
|
||||
token_info = await service.validate_token(request.access_token)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Token validation failed: {e}")
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# Sync user to database
|
||||
user, is_new = await service.sync_user(token_info)
|
||||
|
||||
# Sync roles from groups
|
||||
roles = await service.sync_roles(user, token_info.groups)
|
||||
|
||||
# Commit the transaction
|
||||
await session.commit()
|
||||
|
||||
# Refresh to get relationships
|
||||
await session.refresh(user, ["preferences"])
|
||||
|
||||
# Build response
|
||||
return AuthSyncResponse(
|
||||
user=service.user_to_schema(user),
|
||||
roles=service.roles_to_schema(roles),
|
||||
preferences=service.preferences_to_schema(user.preferences),
|
||||
is_new_user=is_new,
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/users",
|
||||
summary="List all users",
|
||||
response_model=UsersListResponse,
|
||||
responses={
|
||||
200: {"description": "List of users"},
|
||||
},
|
||||
)
|
||||
async def list_users(
|
||||
search: Optional[str] = Query(None, description="Search by name or email"),
|
||||
offset: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(50, ge=1, le=100, description="Maximum records to return"),
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
) -> UsersListResponse:
|
||||
"""
|
||||
List all users who have logged in via Authentik
|
||||
|
||||
Returns paginated list of users with their roles.
|
||||
Supports search filtering by name or email.
|
||||
"""
|
||||
service = AuthService(session)
|
||||
items, total = await service.list_users(
|
||||
search=search,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
return UsersListResponse(items=items, total=total)
|
||||
|
||||
@router.post(
|
||||
"/users/sync-from-authentik",
|
||||
summary="Bulk sync users from Authentik",
|
||||
response_model=BulkSyncResultSchema,
|
||||
responses={
|
||||
200: {"description": "Sync completed"},
|
||||
401: {"description": "Authentik API token invalid"},
|
||||
503: {"description": "Authentik service unavailable"},
|
||||
},
|
||||
)
|
||||
async def sync_users_from_authentik(
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
) -> BulkSyncResultSchema:
|
||||
"""
|
||||
Fetch all users from Authentik and sync to local database
|
||||
|
||||
This endpoint uses the Authentik admin API to fetch all users
|
||||
and create/update them in the local database. Requires
|
||||
AUTHENTIK_CORE_API_TOKEN to be configured.
|
||||
|
||||
Use this to initially populate users or to re-sync after
|
||||
changes in Authentik.
|
||||
"""
|
||||
service = AuthService(session)
|
||||
|
||||
try:
|
||||
result = await service.bulk_sync_from_authentik()
|
||||
logger.info(
|
||||
f"Bulk sync completed: {result.created} created, "
|
||||
f"{result.updated} updated, {result.failed} failed"
|
||||
)
|
||||
return result
|
||||
except ValueError as e:
|
||||
logger.error(f"Bulk sync failed: {e}")
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
summary="Get current user profile",
|
||||
response_model=AuthSyncResponse,
|
||||
responses={
|
||||
200: {"description": "User profile"},
|
||||
401: {"description": "Not authenticated"},
|
||||
},
|
||||
)
|
||||
async def get_me(
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get the current authenticated user's profile
|
||||
|
||||
Note: This endpoint requires a valid session or API key.
|
||||
For now, returns 501 Not Implemented until session management is added.
|
||||
"""
|
||||
# TODO: Implement with get_current_user dependency
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Not implemented - use /auth/sync with access token",
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
# Create controller instance
|
||||
auth_controller = AuthController()
|
||||
Reference in New Issue
Block a user