Include full contents of all nested repositories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
19
letsbe-orchestrator/app/dependencies/__init__.py
Normal file
19
letsbe-orchestrator/app/dependencies/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""FastAPI dependencies for the Orchestrator."""
|
||||
|
||||
from app.dependencies.auth import (
|
||||
CurrentAgentDep,
|
||||
get_current_agent,
|
||||
)
|
||||
from app.dependencies.admin_auth import AdminAuthDep, verify_admin_api_key
|
||||
from app.dependencies.dashboard_auth import DashboardAuthDep, verify_dashboard_token
|
||||
from app.dependencies.local_agent_auth import verify_local_agent_key
|
||||
|
||||
__all__ = [
|
||||
"CurrentAgentDep",
|
||||
"get_current_agent",
|
||||
"AdminAuthDep",
|
||||
"verify_admin_api_key",
|
||||
"DashboardAuthDep",
|
||||
"verify_dashboard_token",
|
||||
"verify_local_agent_key",
|
||||
]
|
||||
33
letsbe-orchestrator/app/dependencies/admin_auth.py
Normal file
33
letsbe-orchestrator/app/dependencies/admin_auth.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Admin authentication dependency for protected endpoints."""
|
||||
|
||||
import secrets
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
async def verify_admin_api_key(
|
||||
x_admin_api_key: str = Header(..., alias="X-Admin-Api-Key"),
|
||||
) -> None:
|
||||
"""
|
||||
Verify admin API key for protected endpoints.
|
||||
|
||||
Used to protect sensitive operations like registration token management.
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if API key is missing or invalid
|
||||
"""
|
||||
settings = get_settings()
|
||||
|
||||
# Use timing-safe comparison to prevent timing attacks
|
||||
if not secrets.compare_digest(x_admin_api_key, settings.ADMIN_API_KEY):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid admin API key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
|
||||
# Dependency that can be used in route decorators
|
||||
AdminAuthDep = Depends(verify_admin_api_key)
|
||||
65
letsbe-orchestrator/app/dependencies/auth.py
Normal file
65
letsbe-orchestrator/app/dependencies/auth.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Agent authentication dependencies."""
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db import AsyncSessionDep
|
||||
from app.models.agent import Agent
|
||||
|
||||
|
||||
async def get_current_agent(
|
||||
db: AsyncSessionDep,
|
||||
x_agent_id: str = Header(..., alias="X-Agent-Id"),
|
||||
x_agent_secret: str = Header(..., alias="X-Agent-Secret"),
|
||||
) -> Agent:
|
||||
"""
|
||||
Validate agent credentials using X-Agent-Id/X-Agent-Secret headers.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
x_agent_id: Agent UUID from X-Agent-Id header
|
||||
x_agent_secret: Agent secret from X-Agent-Secret header
|
||||
|
||||
Returns:
|
||||
Agent if credentials are valid
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if credentials are invalid
|
||||
"""
|
||||
# Parse agent ID
|
||||
try:
|
||||
agent_id = uuid.UUID(x_agent_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid Agent ID format",
|
||||
)
|
||||
|
||||
# Look up agent
|
||||
result = await db.execute(select(Agent).where(Agent.id == agent_id))
|
||||
agent = result.scalar_one_or_none()
|
||||
|
||||
if agent is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid agent credentials",
|
||||
)
|
||||
|
||||
# Verify secret using timing-safe comparison
|
||||
provided_hash = hashlib.sha256(x_agent_secret.encode()).hexdigest()
|
||||
if not secrets.compare_digest(agent.secret_hash, provided_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid agent credentials",
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
# Type alias for dependency injection
|
||||
CurrentAgentDep = Annotated[Agent, Depends(get_current_agent)]
|
||||
73
letsbe-orchestrator/app/dependencies/dashboard_auth.py
Normal file
73
letsbe-orchestrator/app/dependencies/dashboard_auth.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Dashboard authentication dependency for tenant dashboard-to-orchestrator communication."""
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, Path, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db import get_db
|
||||
from app.models.tenant import Tenant
|
||||
|
||||
|
||||
async def verify_dashboard_token(
|
||||
tenant_id: Annotated[UUID, Path(...)],
|
||||
x_dashboard_token: Annotated[str, Header(alias="X-Dashboard-Token")],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Tenant:
|
||||
"""
|
||||
Verify per-tenant dashboard token for tenant dashboard endpoints.
|
||||
|
||||
The dashboard token is used by the tenant's dashboard application
|
||||
(Hub Dashboard or Control Panel) to authenticate requests to the
|
||||
Orchestrator for task execution and status queries.
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant UUID from the path
|
||||
x_dashboard_token: The raw dashboard token from header
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
The verified Tenant object
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if token is missing, invalid, or tenant not found
|
||||
"""
|
||||
# Fetch tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found",
|
||||
)
|
||||
|
||||
if not tenant.dashboard_token_hash:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Dashboard token not configured for this tenant",
|
||||
headers={"WWW-Authenticate": "DashboardToken"},
|
||||
)
|
||||
|
||||
# Compute SHA-256 hash of provided token
|
||||
provided_hash = hashlib.sha256(x_dashboard_token.encode()).hexdigest()
|
||||
|
||||
# Use timing-safe comparison to prevent timing attacks
|
||||
if not secrets.compare_digest(tenant.dashboard_token_hash, provided_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid dashboard token",
|
||||
headers={"WWW-Authenticate": "DashboardToken"},
|
||||
)
|
||||
|
||||
return tenant
|
||||
|
||||
|
||||
# Type alias for dependency injection
|
||||
DashboardAuthDep = Annotated[Tenant, Depends(verify_dashboard_token)]
|
||||
49
letsbe-orchestrator/app/dependencies/local_agent_auth.py
Normal file
49
letsbe-orchestrator/app/dependencies/local_agent_auth.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Local agent authentication dependency for LOCAL_MODE registration."""
|
||||
|
||||
import secrets
|
||||
|
||||
from fastapi import Header, HTTPException, status
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
async def verify_local_agent_key(
|
||||
x_local_agent_key: str = Header(..., alias="X-Local-Agent-Key"),
|
||||
) -> None:
|
||||
"""
|
||||
Verify LOCAL_AGENT_KEY for local agent registration.
|
||||
|
||||
This is a narrow-scope credential that can ONLY register the local agent.
|
||||
It is NOT the same as ADMIN_API_KEY.
|
||||
|
||||
HTTP Status Codes:
|
||||
- 404: LOCAL_MODE is false (endpoint hidden by design)
|
||||
- 401: LOCAL_AGENT_KEY is missing, invalid, or not configured
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 if not in LOCAL_MODE, 401 if key is invalid
|
||||
"""
|
||||
settings = get_settings()
|
||||
|
||||
# Endpoint only exists in LOCAL_MODE (security by obscurity)
|
||||
if not settings.LOCAL_MODE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Not found",
|
||||
)
|
||||
|
||||
# LOCAL_AGENT_KEY must be configured
|
||||
if not settings.LOCAL_AGENT_KEY:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Local agent key not configured",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
# Use timing-safe comparison to prevent timing attacks
|
||||
if not secrets.compare_digest(x_local_agent_key, settings.LOCAL_AGENT_KEY):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid local agent key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
Reference in New Issue
Block a user