GT AI OS Community Edition v2.0.33
Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
457
apps/resource-cluster/app/core/capability_auth.py
Normal file
457
apps/resource-cluster/app/core/capability_auth.py
Normal file
@@ -0,0 +1,457 @@
|
||||
"""
|
||||
Capability-Based Authentication for GT 2.0 Resource Cluster
|
||||
|
||||
Implements JWT capability token verification with:
|
||||
- Cryptographic signature validation
|
||||
- Fine-grained resource permissions
|
||||
- Rate limiting and constraints enforcement
|
||||
- Tenant isolation validation
|
||||
- Zero external dependencies
|
||||
|
||||
GT 2.0 Security Principles:
|
||||
- Self-contained: No external auth services
|
||||
- Stateless: All permissions in JWT token
|
||||
- Cryptographic: RSA signature verification
|
||||
- Isolated: Perfect tenant separation
|
||||
"""
|
||||
|
||||
import jwt
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import HTTPException, Depends, Header
|
||||
from app.core.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class CapabilityError(Exception):
|
||||
"""Capability authentication error"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceType(str, Enum):
|
||||
"""Resource types in GT 2.0"""
|
||||
LLM = "llm"
|
||||
EMBEDDING = "embedding"
|
||||
VECTOR_STORAGE = "vector_storage"
|
||||
EXTERNAL_SERVICES = "external_services"
|
||||
ADMIN = "admin"
|
||||
|
||||
|
||||
class ActionType(str, Enum):
|
||||
"""Action types for resources"""
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
EXECUTE = "execute"
|
||||
ADMIN = "admin"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Capability:
|
||||
"""Individual capability definition"""
|
||||
resource: ResourceType
|
||||
actions: List[ActionType]
|
||||
constraints: Dict[str, Any]
|
||||
expires_at: Optional[datetime] = None
|
||||
|
||||
def allows_action(self, action: ActionType) -> bool:
|
||||
"""Check if capability allows specific action"""
|
||||
return action in self.actions
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if capability is expired"""
|
||||
if not self.expires_at:
|
||||
return False
|
||||
return datetime.now(timezone.utc) > self.expires_at
|
||||
|
||||
def check_constraint(self, constraint_name: str, value: Any) -> bool:
|
||||
"""Check if value satisfies constraint"""
|
||||
if constraint_name not in self.constraints:
|
||||
return True # No constraint means allowed
|
||||
|
||||
constraint_value = self.constraints[constraint_name]
|
||||
|
||||
if constraint_name == "max_tokens":
|
||||
return value <= constraint_value
|
||||
elif constraint_name == "allowed_models":
|
||||
return value in constraint_value
|
||||
elif constraint_name == "max_requests_per_hour":
|
||||
# This would be checked separately with rate limiting
|
||||
return True
|
||||
elif constraint_name == "allowed_tenants":
|
||||
return value in constraint_value
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class CapabilityToken:
|
||||
"""Parsed capability token"""
|
||||
subject: str
|
||||
tenant_id: str
|
||||
capabilities: List[Capability]
|
||||
issued_at: datetime
|
||||
expires_at: datetime
|
||||
issuer: str
|
||||
token_version: str
|
||||
|
||||
def has_capability(self, resource: ResourceType, action: ActionType) -> bool:
|
||||
"""Check if token has specific capability"""
|
||||
for cap in self.capabilities:
|
||||
if cap.resource == resource and cap.allows_action(action) and not cap.is_expired():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_capability(self, resource: ResourceType) -> Optional[Capability]:
|
||||
"""Get capability for specific resource"""
|
||||
for cap in self.capabilities:
|
||||
if cap.resource == resource and not cap.is_expired():
|
||||
return cap
|
||||
return None
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if entire token is expired"""
|
||||
return datetime.now(timezone.utc) > self.expires_at
|
||||
|
||||
|
||||
class CapabilityAuthenticator:
|
||||
"""
|
||||
Handles capability token verification and authorization.
|
||||
|
||||
Uses JWT tokens with embedded permissions for stateless authentication.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.settings = get_settings()
|
||||
|
||||
# In production, this would be loaded from secure storage
|
||||
# For development, using the secret key
|
||||
self.secret_key = self.settings.secret_key
|
||||
self.algorithm = "HS256" # TODO: Upgrade to RS256 with public/private keys
|
||||
|
||||
logger.info("Capability authenticator initialized")
|
||||
|
||||
async def verify_token(self, token: str) -> CapabilityToken:
|
||||
"""
|
||||
Verify and parse capability token.
|
||||
|
||||
Args:
|
||||
token: JWT capability token
|
||||
|
||||
Returns:
|
||||
Parsed capability token
|
||||
|
||||
Raises:
|
||||
CapabilityError: If token is invalid or expired
|
||||
"""
|
||||
try:
|
||||
# Decode JWT token
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
self.secret_key,
|
||||
algorithms=[self.algorithm],
|
||||
audience="gt2-resource-cluster"
|
||||
)
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ["sub", "tenant_id", "capabilities", "iat", "exp", "iss"]
|
||||
for field in required_fields:
|
||||
if field not in payload:
|
||||
raise CapabilityError(f"Missing required field: {field}")
|
||||
|
||||
# Parse timestamps
|
||||
issued_at = datetime.fromtimestamp(payload["iat"], tz=timezone.utc)
|
||||
expires_at = datetime.fromtimestamp(payload["exp"], tz=timezone.utc)
|
||||
|
||||
# Check token expiration
|
||||
if datetime.now(timezone.utc) > expires_at:
|
||||
raise CapabilityError("Token has expired")
|
||||
|
||||
# Parse capabilities
|
||||
capabilities = []
|
||||
for cap_data in payload["capabilities"]:
|
||||
try:
|
||||
capability = Capability(
|
||||
resource=ResourceType(cap_data["resource"]),
|
||||
actions=[ActionType(action) for action in cap_data["actions"]],
|
||||
constraints=cap_data.get("constraints", {}),
|
||||
expires_at=datetime.fromtimestamp(
|
||||
cap_data["expires_at"], tz=timezone.utc
|
||||
) if cap_data.get("expires_at") else None
|
||||
)
|
||||
capabilities.append(capability)
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.warning(f"Invalid capability in token: {e}")
|
||||
# Skip invalid capabilities rather than rejecting entire token
|
||||
continue
|
||||
|
||||
# Create capability token
|
||||
capability_token = CapabilityToken(
|
||||
subject=payload["sub"],
|
||||
tenant_id=payload["tenant_id"],
|
||||
capabilities=capabilities,
|
||||
issued_at=issued_at,
|
||||
expires_at=expires_at,
|
||||
issuer=payload["iss"],
|
||||
token_version=payload.get("token_version", "1.0")
|
||||
)
|
||||
|
||||
logger.debug(f"Capability token verified for {capability_token.subject}")
|
||||
return capability_token
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise CapabilityError("Token has expired")
|
||||
except jwt.InvalidTokenError as e:
|
||||
raise CapabilityError(f"Invalid token: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification failed: {e}")
|
||||
raise CapabilityError(f"Token verification failed: {e}")
|
||||
|
||||
async def check_resource_access(
|
||||
self,
|
||||
capability_token: CapabilityToken,
|
||||
resource: ResourceType,
|
||||
action: ActionType,
|
||||
constraints: Optional[Dict[str, Any]] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if token allows access to resource with specific action.
|
||||
|
||||
Args:
|
||||
capability_token: Verified capability token
|
||||
resource: Resource type to access
|
||||
action: Action to perform
|
||||
constraints: Additional constraints to check
|
||||
|
||||
Returns:
|
||||
True if access is allowed
|
||||
|
||||
Raises:
|
||||
CapabilityError: If access is denied
|
||||
"""
|
||||
try:
|
||||
# Check token expiration
|
||||
if capability_token.is_expired():
|
||||
raise CapabilityError("Token has expired")
|
||||
|
||||
# Find matching capability
|
||||
capability = capability_token.get_capability(resource)
|
||||
if not capability:
|
||||
raise CapabilityError(f"No capability for resource: {resource}")
|
||||
|
||||
# Check action permission
|
||||
if not capability.allows_action(action):
|
||||
raise CapabilityError(f"Action {action} not allowed for resource {resource}")
|
||||
|
||||
# Check constraints if provided
|
||||
if constraints:
|
||||
for constraint_name, value in constraints.items():
|
||||
if not capability.check_constraint(constraint_name, value):
|
||||
raise CapabilityError(
|
||||
f"Constraint violation: {constraint_name} = {value}"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except CapabilityError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Resource access check failed: {e}")
|
||||
raise CapabilityError(f"Access check failed: {e}")
|
||||
|
||||
|
||||
# Global authenticator instance
|
||||
capability_authenticator = CapabilityAuthenticator()
|
||||
|
||||
|
||||
async def verify_capability_token(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify capability token and return payload.
|
||||
|
||||
Args:
|
||||
token: JWT capability token
|
||||
|
||||
Returns:
|
||||
Token payload as dictionary
|
||||
|
||||
Raises:
|
||||
CapabilityError: If token is invalid
|
||||
"""
|
||||
capability_token = await capability_authenticator.verify_token(token)
|
||||
|
||||
return {
|
||||
"sub": capability_token.subject,
|
||||
"tenant_id": capability_token.tenant_id,
|
||||
"capabilities": [
|
||||
{
|
||||
"resource": cap.resource.value,
|
||||
"actions": [action.value for action in cap.actions],
|
||||
"constraints": cap.constraints
|
||||
}
|
||||
for cap in capability_token.capabilities
|
||||
],
|
||||
"iat": capability_token.issued_at.timestamp(),
|
||||
"exp": capability_token.expires_at.timestamp(),
|
||||
"iss": capability_token.issuer,
|
||||
"token_version": capability_token.token_version
|
||||
}
|
||||
|
||||
|
||||
async def get_current_capability(
|
||||
authorization: str = Header(..., description="Bearer token")
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
FastAPI dependency to get current capability from Authorization header.
|
||||
|
||||
Args:
|
||||
authorization: Authorization header with Bearer token
|
||||
|
||||
Returns:
|
||||
Capability payload
|
||||
|
||||
Raises:
|
||||
HTTPException: If authentication fails
|
||||
"""
|
||||
try:
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid authorization header format"
|
||||
)
|
||||
|
||||
token = authorization[7:] # Remove "Bearer " prefix
|
||||
payload = await verify_capability_token(token)
|
||||
|
||||
return payload
|
||||
|
||||
except CapabilityError as e:
|
||||
logger.warning(f"Capability authentication failed: {e}")
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Authentication error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Authentication error")
|
||||
|
||||
|
||||
async def require_capability(
|
||||
resource: ResourceType,
|
||||
action: ActionType,
|
||||
constraints: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
FastAPI dependency to require specific capability.
|
||||
|
||||
Args:
|
||||
resource: Required resource type
|
||||
action: Required action type
|
||||
constraints: Additional constraints to check
|
||||
|
||||
Returns:
|
||||
Dependency function
|
||||
"""
|
||||
async def _check_capability(
|
||||
capability_payload: Dict[str, Any] = Depends(get_current_capability)
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
# Reconstruct capability token from payload
|
||||
capabilities = []
|
||||
for cap_data in capability_payload["capabilities"]:
|
||||
capability = Capability(
|
||||
resource=ResourceType(cap_data["resource"]),
|
||||
actions=[ActionType(action) for action in cap_data["actions"]],
|
||||
constraints=cap_data["constraints"]
|
||||
)
|
||||
capabilities.append(capability)
|
||||
|
||||
capability_token = CapabilityToken(
|
||||
subject=capability_payload["sub"],
|
||||
tenant_id=capability_payload["tenant_id"],
|
||||
capabilities=capabilities,
|
||||
issued_at=datetime.fromtimestamp(capability_payload["iat"], tz=timezone.utc),
|
||||
expires_at=datetime.fromtimestamp(capability_payload["exp"], tz=timezone.utc),
|
||||
issuer=capability_payload["iss"],
|
||||
token_version=capability_payload["token_version"]
|
||||
)
|
||||
|
||||
# Check required capability
|
||||
await capability_authenticator.check_resource_access(
|
||||
capability_token=capability_token,
|
||||
resource=resource,
|
||||
action=action,
|
||||
constraints=constraints
|
||||
)
|
||||
|
||||
return capability_payload
|
||||
|
||||
except CapabilityError as e:
|
||||
logger.warning(f"Capability check failed: {e}")
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Capability check error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Authorization error")
|
||||
|
||||
return _check_capability
|
||||
|
||||
|
||||
# Convenience functions for common capability checks
|
||||
|
||||
async def require_llm_capability(
|
||||
capability_payload: Dict[str, Any] = Depends(
|
||||
require_capability(ResourceType.LLM, ActionType.EXECUTE)
|
||||
)
|
||||
) -> Dict[str, Any]:
|
||||
"""Require LLM execution capability"""
|
||||
return capability_payload
|
||||
|
||||
|
||||
async def require_embedding_capability(
|
||||
capability_payload: Dict[str, Any] = Depends(
|
||||
require_capability(ResourceType.EMBEDDING, ActionType.EXECUTE)
|
||||
)
|
||||
) -> Dict[str, Any]:
|
||||
"""Require embedding generation capability"""
|
||||
return capability_payload
|
||||
|
||||
|
||||
async def require_admin_capability(
|
||||
capability_payload: Dict[str, Any] = Depends(
|
||||
require_capability(ResourceType.ADMIN, ActionType.ADMIN)
|
||||
)
|
||||
) -> Dict[str, Any]:
|
||||
"""Require admin capability"""
|
||||
return capability_payload
|
||||
|
||||
|
||||
async def verify_capability_token_dependency(
|
||||
authorization: str = Header(..., description="Bearer token")
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
FastAPI dependency for ChromaDB MCP API that verifies capability token.
|
||||
|
||||
Returns token payload with raw_token field for service layer use.
|
||||
"""
|
||||
try:
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid authorization header format"
|
||||
)
|
||||
|
||||
token = authorization[7:] # Remove "Bearer " prefix
|
||||
payload = await verify_capability_token(token)
|
||||
|
||||
# Add raw token for service layer
|
||||
payload["raw_token"] = token
|
||||
|
||||
return payload
|
||||
|
||||
except CapabilityError as e:
|
||||
logger.warning(f"Capability authentication failed: {e}")
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Authentication error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Authentication error")
|
||||
Reference in New Issue
Block a user