6aea953734
- Add SpawnService with container lifecycle (spawn/stop/status) - Generate Docker Compose services from tool manifests - Integrate Traefik label generation with subdomain routing - Mount workspace, config, and SSH key volumes - Add container status polling and health checks - Enhance tool instance API with spawn/stop/start/status endpoints - Add Traefik forwardAuth middleware for auth proxy - Update code-server manifest with runtime configuration
137 lines
4.1 KiB
Python
137 lines
4.1 KiB
Python
from fastapi import Depends, HTTPException, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.auth.jwt import decode_token
|
|
from app.config import settings
|
|
from app.db import get_db_session
|
|
from app.models.user import User
|
|
|
|
bearer_scheme = HTTPBearer(auto_error=False)
|
|
|
|
|
|
async def _get_or_create_dev_user(session: AsyncSession) -> User:
|
|
"""Return or create the fixed development user."""
|
|
result = await session.execute(
|
|
select(User).where(User.authentik_sub == "dev-user")
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
if user is None:
|
|
user = User(
|
|
authentik_sub="dev-user",
|
|
email="dev@localhost",
|
|
display_name="Dev User",
|
|
is_active=True,
|
|
)
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
return user
|
|
|
|
|
|
async def get_current_user(
|
|
token: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> User:
|
|
if token is None:
|
|
if settings.debug and settings.auth_dev_bypass:
|
|
return await _get_or_create_dev_user(session)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Not authenticated",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
try:
|
|
claims = decode_token(token.credentials)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=f"Invalid token: {exc}",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
) from exc
|
|
|
|
authentik_sub = claims.get("sub")
|
|
email = claims.get("email", "")
|
|
display_name = claims.get("name") or claims.get("preferred_username") or email
|
|
|
|
if not authentik_sub:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token missing 'sub' claim",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
result = await session.execute(
|
|
select(User).where(User.authentik_sub == authentik_sub)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
|
|
if user is None:
|
|
user = User(
|
|
authentik_sub=authentik_sub,
|
|
email=email,
|
|
display_name=display_name,
|
|
is_active=True,
|
|
)
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
|
|
return user
|
|
|
|
|
|
async def get_current_active_user(
|
|
current_user: User = Depends(get_current_user),
|
|
) -> User:
|
|
if not current_user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Inactive user",
|
|
)
|
|
return current_user
|
|
|
|
|
|
async def validate_traefik_auth(
|
|
token: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> User:
|
|
if token is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Not authenticated",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
try:
|
|
claims = decode_token(token.credentials)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=f"Invalid token: {exc}",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
) from exc
|
|
|
|
authentik_sub = claims.get("sub")
|
|
if not authentik_sub:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token missing 'sub' claim",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
result = await session.execute(
|
|
select(User).where(User.authentik_sub == authentik_sub)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
|
|
if user is None or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not found or inactive",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
return user
|