Build and Push / build (release) Successful in 50s
- GET /auth/groups - list groups with search/pagination - POST /auth/groups/sync-from-authentik - bulk sync from Authentik - Group model and database migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
247 lines
9.1 KiB
Python
247 lines
9.1 KiB
Python
"""
|
|
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, GroupsListResponse
|
|
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(
|
|
"/groups",
|
|
summary="List all groups",
|
|
response_model=GroupsListResponse,
|
|
responses={
|
|
200: {"description": "List of groups"},
|
|
},
|
|
)
|
|
async def list_groups(
|
|
search: Optional[str] = Query(None, description="Search by group name"),
|
|
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),
|
|
) -> GroupsListResponse:
|
|
"""
|
|
List all groups synced from Authentik
|
|
|
|
Returns paginated list of groups with their details.
|
|
Supports search filtering by name.
|
|
"""
|
|
service = AuthService(session)
|
|
items, total = await service.list_groups(
|
|
search=search,
|
|
offset=offset,
|
|
limit=limit,
|
|
)
|
|
return GroupsListResponse(items=items, total=total)
|
|
|
|
@router.post(
|
|
"/groups/sync-from-authentik",
|
|
summary="Bulk sync groups from Authentik",
|
|
response_model=BulkSyncResultSchema,
|
|
responses={
|
|
200: {"description": "Sync completed"},
|
|
401: {"description": "Authentik API credentials invalid"},
|
|
503: {"description": "Authentik service unavailable"},
|
|
},
|
|
)
|
|
async def sync_groups_from_authentik(
|
|
session: AsyncSession = Depends(get_async_session),
|
|
) -> BulkSyncResultSchema:
|
|
"""
|
|
Fetch all groups from Authentik and sync to local database
|
|
|
|
This endpoint uses the Authentik admin API to fetch all groups
|
|
and create/update them in the local database. Requires
|
|
AUTHENTIK_USERNAME and AUTHENTIK_PASSWORD to be configured.
|
|
|
|
Use this to populate groups or to re-sync after changes in Authentik.
|
|
"""
|
|
service = AuthService(session)
|
|
|
|
try:
|
|
result = await service.bulk_sync_groups_from_authentik()
|
|
logger.info(
|
|
f"Groups bulk sync completed: {result.created} created, "
|
|
f"{result.updated} updated, {result.failed} failed"
|
|
)
|
|
return result
|
|
except ValueError as e:
|
|
logger.error(f"Groups 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()
|