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:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
"""
GT 2.0 API Standards - Capability-Based REST (CB-REST)
A simple, secure API standard designed for GT 2.0's philosophy of
"Elegant Simplicity Through Intelligent Architecture"
"""
from .response import StandardResponse, StandardError, format_response, format_error
from .capability import (
Capability,
CapabilityVerifier,
verify_capability,
require_capability,
extract_capability_from_jwt
)
from .errors import ErrorCode, APIError, error_responses
from .middleware import (
CapabilityMiddleware,
RequestCorrelationMiddleware,
TenantIsolationMiddleware
)
__all__ = [
# Response formatting
'StandardResponse',
'StandardError',
'format_response',
'format_error',
# Capability verification
'Capability',
'CapabilityVerifier',
'verify_capability',
'require_capability',
'extract_capability_from_jwt',
# Error handling
'ErrorCode',
'APIError',
'error_responses',
# Middleware
'CapabilityMiddleware',
'RequestCorrelationMiddleware',
'TenantIsolationMiddleware'
]
__version__ = '1.0.0'

View File

@@ -0,0 +1,337 @@
"""
Capability-based access control for GT 2.0 CB-REST API
Design principle: Security through cryptographic capabilities embedded in JWT tokens
Perfect tenant isolation through capability scoping
"""
import hashlib
import hmac
import json
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
import jwt
from pydantic import BaseModel, Field
from fastapi import Depends, HTTPException, Request, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .errors import ErrorCode, APIError
# Security scheme for FastAPI
security = HTTPBearer()
class Capability(BaseModel):
"""
A single capability grant
Format: resource:id:action
Example: tenant:customer1:read
"""
resource: str = Field(..., description="Resource type (tenant, user, resource, etc.)")
resource_id: str = Field(..., description="Specific resource ID or wildcard (*)")
action: str = Field(..., description="Action allowed (read, write, create, delete, admin)")
constraints: Optional[Dict[str, Any]] = Field(None, description="Additional constraints")
expires_at: Optional[datetime] = Field(None, description="Expiration time for this capability")
def matches(self, required_resource: str, required_id: str, required_action: str) -> bool:
"""
Check if this capability matches the required permission
Supports wildcards (*) for resource_id and action
"""
# Check resource type
if self.resource != required_resource and self.resource != "*":
return False
# Check resource ID (supports wildcards)
if self.resource_id != required_id and self.resource_id != "*":
return False
# Check action (supports wildcards and hierarchical permissions)
if self.action == "*" or self.action == "admin":
return True # Admin has all permissions
if self.action == required_action:
return True
# Hierarchical permissions: write includes read, delete includes write
action_hierarchy = {
"delete": ["write", "read"],
"write": ["read"],
"create": ["read"]
}
if required_action in action_hierarchy.get(self.action, []):
return True
return False
def to_string(self) -> str:
"""Convert capability to string format"""
return f"{self.resource}:{self.resource_id}:{self.action}"
@classmethod
def from_string(cls, capability_str: str) -> "Capability":
"""Parse capability from string format"""
parts = capability_str.split(":")
if len(parts) != 3:
raise ValueError(f"Invalid capability format: {capability_str}")
return cls(
resource=parts[0],
resource_id=parts[1],
action=parts[2]
)
class CapabilityToken(BaseModel):
"""
JWT token payload with embedded capabilities
"""
sub: str = Field(..., description="Subject (user email or ID)")
tenant_id: str = Field(..., description="Tenant ID for isolation")
user_type: str = Field(..., description="User type (super_admin, gt_admin, tenant_admin, tenant_user)")
capabilities: List[Dict[str, Any]] = Field(..., description="List of granted capabilities")
iat: int = Field(..., description="Issued at timestamp")
exp: int = Field(..., description="Expiration timestamp")
jti: Optional[str] = Field(None, description="JWT ID for revocation")
def get_capabilities(self) -> List[Capability]:
"""Convert capability dicts to Capability objects"""
return [Capability(**cap) for cap in self.capabilities]
def has_capability(self, resource: str, resource_id: str, action: str) -> bool:
"""Check if token has a specific capability"""
for cap in self.get_capabilities():
if cap.matches(resource, resource_id, action):
# Check expiration if set
if cap.expires_at and cap.expires_at < datetime.utcnow():
continue
return True
return False
class CapabilityVerifier:
"""
Verifies capabilities and signatures
"""
def __init__(self, secret_key: str, algorithm: str = "HS256"):
self.secret_key = secret_key
self.algorithm = algorithm
def verify_token(self, token: str) -> CapabilityToken:
"""
Verify and decode a JWT capability token
Args:
token: JWT token string
Returns:
Decoded CapabilityToken
Raises:
APIError: If token is invalid or expired
"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return CapabilityToken(**payload)
except jwt.ExpiredSignatureError:
raise APIError(
code=ErrorCode.CAPABILITY_EXPIRED,
message="Capability token has expired",
status_code=401
)
except jwt.InvalidTokenError as e:
raise APIError(
code=ErrorCode.CAPABILITY_INVALID,
message=f"Invalid capability token: {str(e)}",
status_code=401
)
def verify_signature(self, token: str, signature: str) -> bool:
"""
Verify HMAC signature of capability
Args:
token: JWT token string
signature: HMAC signature to verify
Returns:
True if signature is valid
"""
expected_signature = hmac.new(
self.secret_key.encode(),
token.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
def create_token(
self,
user_email: str,
tenant_id: str,
user_type: str,
capabilities: List[Capability],
expires_in: timedelta = timedelta(hours=24)
) -> str:
"""
Create a new capability token
Args:
user_email: User's email address
tenant_id: Tenant ID for isolation
user_type: Type of user
capabilities: List of capabilities to grant
expires_in: Token expiration time
Returns:
JWT token string
"""
now = datetime.utcnow()
exp = now + expires_in
payload = CapabilityToken(
sub=user_email,
tenant_id=tenant_id,
user_type=user_type,
capabilities=[cap.dict(exclude_none=True) for cap in capabilities],
iat=int(now.timestamp()),
exp=int(exp.timestamp())
)
return jwt.encode(
payload.dict(exclude_none=True),
self.secret_key,
algorithm=self.algorithm
)
# Global verifier instance (initialized by application)
_verifier: Optional[CapabilityVerifier] = None
def init_capability_verifier(secret_key: str, algorithm: str = "HS256"):
"""Initialize the global capability verifier"""
global _verifier
_verifier = CapabilityVerifier(secret_key, algorithm)
def get_verifier() -> CapabilityVerifier:
"""Get the global capability verifier"""
if _verifier is None:
raise RuntimeError("Capability verifier not initialized. Call init_capability_verifier first.")
return _verifier
async def verify_capability(
credentials: HTTPAuthorizationCredentials = Security(security),
request: Request = None
) -> CapabilityToken:
"""
FastAPI dependency to verify capability token
Args:
credentials: Bearer token from Authorization header
request: Optional request object for additional context
Returns:
Verified CapabilityToken
Raises:
HTTPException: If token is invalid
"""
verifier = get_verifier()
# Check for signature header if request provided
signature = None
if request:
signature = request.headers.get("X-Capability-Signature")
if signature and not verifier.verify_signature(credentials.credentials, signature):
raise APIError(
code=ErrorCode.CAPABILITY_SIGNATURE_INVALID,
message="Capability signature verification failed",
status_code=401
)
return verifier.verify_token(credentials.credentials)
def require_capability(
resource: str,
resource_id: str,
action: str
):
"""
Create a FastAPI dependency that requires a specific capability
Args:
resource: Resource type
resource_id: Resource ID (can be "*" for wildcard)
action: Required action
Returns:
FastAPI dependency function
"""
async def capability_checker(
token: CapabilityToken = Depends(verify_capability)
) -> CapabilityToken:
"""Check if the token has the required capability"""
# Super admins bypass all checks (GT 2.0 admin efficiency)
if token.user_type == "super_admin":
return token
# GT admins can access all tenants but not system resources
if token.user_type == "gt_admin" and resource in ["tenant", "user", "resource"]:
return token
# Check specific capability
if not token.has_capability(resource, resource_id, action):
capability_required = f"{resource}:{resource_id}:{action}"
# Find what capabilities the user has for this resource
user_caps = [
cap.to_string()
for cap in token.get_capabilities()
if cap.resource == resource
]
raise APIError(
code=ErrorCode.CAPABILITY_INSUFFICIENT,
message=f"Insufficient capability for {resource} {action}",
status_code=403,
capability_required=capability_required,
capability_provided=", ".join(user_caps) if user_caps else "none"
)
return token
return capability_checker
def extract_capability_from_jwt(token_str: str) -> Optional[CapabilityToken]:
"""
Extract capability token without verification (for logging/debugging)
WARNING: This does not verify the token! Use only for non-security purposes.
Args:
token_str: JWT token string
Returns:
CapabilityToken if decodable, None otherwise
"""
try:
# Decode without verification
payload = jwt.decode(token_str, options={"verify_signature": False})
return CapabilityToken(**payload)
except Exception:
return None

View File

@@ -0,0 +1,207 @@
"""
Standard error codes for GT 2.0 CB-REST API
Design principle: Simple, clear error codes that directly indicate the issue
No complex hierarchies, just straightforward categories
"""
from enum import Enum
from typing import Dict, Any, Optional
from fastapi import HTTPException, status
class ErrorCode(str, Enum):
"""
Standard error codes following GT 2.0 philosophy:
- Simple and clear
- Security-focused
- Tenant-aware
"""
# Capability errors (security by design)
CAPABILITY_INSUFFICIENT = "CAPABILITY_INSUFFICIENT"
CAPABILITY_INVALID = "CAPABILITY_INVALID"
CAPABILITY_EXPIRED = "CAPABILITY_EXPIRED"
CAPABILITY_SIGNATURE_INVALID = "CAPABILITY_SIGNATURE_INVALID"
# Resource errors
RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
RESOURCE_LOCKED = "RESOURCE_LOCKED"
RESOURCE_QUOTA_EXCEEDED = "RESOURCE_QUOTA_EXCEEDED"
# Tenant isolation errors
TENANT_ISOLATED = "TENANT_ISOLATED"
TENANT_NOT_FOUND = "TENANT_NOT_FOUND"
TENANT_SUSPENDED = "TENANT_SUSPENDED"
TENANT_QUOTA_EXCEEDED = "TENANT_QUOTA_EXCEEDED"
# Request errors
INVALID_REQUEST = "INVALID_REQUEST"
VALIDATION_FAILED = "VALIDATION_FAILED"
MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD"
INVALID_FIELD_VALUE = "INVALID_FIELD_VALUE"
# System errors (minimal, as per GT 2.0 philosophy)
SYSTEM_ERROR = "SYSTEM_ERROR"
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
# File-based database specific errors
FILE_LOCK_TIMEOUT = "FILE_LOCK_TIMEOUT"
FILE_CORRUPTION = "FILE_CORRUPTION"
ENCRYPTION_FAILED = "ENCRYPTION_FAILED"
class APIError(HTTPException):
"""
Standard API error that integrates with FastAPI
Simplifies error handling while maintaining consistency
"""
def __init__(
self,
code: ErrorCode,
message: str,
status_code: int = status.HTTP_400_BAD_REQUEST,
capability_required: Optional[str] = None,
capability_provided: Optional[str] = None,
details: Optional[Dict[str, Any]] = None
):
self.code = code
self.message = message
self.capability_required = capability_required
self.capability_provided = capability_provided
self.details = details or {}
# Create the detail structure for HTTPException
detail = {
"code": code.value,
"message": message,
"capability_required": capability_required,
"capability_provided": capability_provided,
"details": self.details
}
# Remove None values for cleaner response
detail = {k: v for k, v in detail.items() if v is not None}
super().__init__(status_code=status_code, detail=detail)
# Pre-defined error responses for common scenarios
error_responses: Dict[ErrorCode, Dict[str, Any]] = {
ErrorCode.CAPABILITY_INSUFFICIENT: {
"status_code": status.HTTP_403_FORBIDDEN,
"description": "User lacks required capability for this operation"
},
ErrorCode.CAPABILITY_INVALID: {
"status_code": status.HTTP_401_UNAUTHORIZED,
"description": "Invalid capability token"
},
ErrorCode.CAPABILITY_EXPIRED: {
"status_code": status.HTTP_401_UNAUTHORIZED,
"description": "Capability token has expired"
},
ErrorCode.CAPABILITY_SIGNATURE_INVALID: {
"status_code": status.HTTP_401_UNAUTHORIZED,
"description": "Capability signature verification failed"
},
ErrorCode.RESOURCE_NOT_FOUND: {
"status_code": status.HTTP_404_NOT_FOUND,
"description": "Requested resource does not exist"
},
ErrorCode.RESOURCE_ALREADY_EXISTS: {
"status_code": status.HTTP_409_CONFLICT,
"description": "Resource already exists"
},
ErrorCode.RESOURCE_LOCKED: {
"status_code": status.HTTP_423_LOCKED,
"description": "Resource is locked"
},
ErrorCode.RESOURCE_QUOTA_EXCEEDED: {
"status_code": status.HTTP_429_TOO_MANY_REQUESTS,
"description": "Resource quota exceeded"
},
ErrorCode.TENANT_ISOLATED: {
"status_code": status.HTTP_403_FORBIDDEN,
"description": "Cross-tenant access attempted"
},
ErrorCode.TENANT_NOT_FOUND: {
"status_code": status.HTTP_404_NOT_FOUND,
"description": "Tenant does not exist"
},
ErrorCode.TENANT_SUSPENDED: {
"status_code": status.HTTP_403_FORBIDDEN,
"description": "Tenant is suspended"
},
ErrorCode.TENANT_QUOTA_EXCEEDED: {
"status_code": status.HTTP_429_TOO_MANY_REQUESTS,
"description": "Tenant quota exceeded"
},
ErrorCode.INVALID_REQUEST: {
"status_code": status.HTTP_400_BAD_REQUEST,
"description": "Invalid request format"
},
ErrorCode.VALIDATION_FAILED: {
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
"description": "Request validation failed"
},
ErrorCode.MISSING_REQUIRED_FIELD: {
"status_code": status.HTTP_400_BAD_REQUEST,
"description": "Required field is missing"
},
ErrorCode.INVALID_FIELD_VALUE: {
"status_code": status.HTTP_400_BAD_REQUEST,
"description": "Invalid field value"
},
ErrorCode.SYSTEM_ERROR: {
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"description": "Internal system error"
},
ErrorCode.SERVICE_UNAVAILABLE: {
"status_code": status.HTTP_503_SERVICE_UNAVAILABLE,
"description": "Service temporarily unavailable"
},
ErrorCode.FILE_LOCK_TIMEOUT: {
"status_code": status.HTTP_423_LOCKED,
"description": "File database lock timeout"
},
ErrorCode.FILE_CORRUPTION: {
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"description": "File database corruption detected"
},
ErrorCode.ENCRYPTION_FAILED: {
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"description": "Encryption operation failed"
}
}
def raise_api_error(
code: ErrorCode,
message: Optional[str] = None,
**kwargs
) -> None:
"""
Convenience function to raise a standard API error
Args:
code: Error code
message: Optional custom message (uses default if not provided)
**kwargs: Additional error details
Raises:
APIError
"""
error_info = error_responses.get(code, {})
if message is None:
message = error_info.get("description", "An error occurred")
raise APIError(
code=code,
message=message,
status_code=error_info.get("status_code", status.HTTP_400_BAD_REQUEST),
**kwargs
)

View File

@@ -0,0 +1,329 @@
"""
Standard middleware for GT 2.0 CB-REST API
Design principle: Cross-cutting concerns handled simply and consistently
"""
import uuid
import time
import logging
from typing import Optional, Dict, Any, Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
from .capability import verify_capability, CapabilityToken, get_verifier
from .response import format_error
from .errors import ErrorCode
logger = logging.getLogger(__name__)
class RequestCorrelationMiddleware(BaseHTTPMiddleware):
"""
Add request correlation ID to all requests for distributed tracing
GT 2.0 Philosophy: Simple observability through request tracking
"""
def __init__(self, app: ASGIApp, header_name: str = "X-Request-ID"):
super().__init__(app)
self.header_name = header_name
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Get or generate request ID
request_id = request.headers.get(self.header_name)
if not request_id:
request_id = str(uuid.uuid4())
# Store in request state for access by handlers
request.state.request_id = request_id
# Process request
start_time = time.time()
response = await call_next(request)
duration = time.time() - start_time
# Add headers to response
response.headers[self.header_name] = request_id
response.headers["X-Response-Time"] = f"{duration:.3f}s"
# Log request completion
logger.info(
f"Request {request_id} completed",
extra={
"request_id": request_id,
"method": request.method,
"path": request.url.path,
"status": response.status_code,
"duration": duration
}
)
return response
class CapabilityMiddleware(BaseHTTPMiddleware):
"""
Verify capabilities for all protected endpoints
GT 2.0 Philosophy: Security by design, not configuration
"""
def __init__(
self,
app: ASGIApp,
exclude_paths: Optional[list[str]] = None,
audit_logger: Optional[logging.Logger] = None
):
super().__init__(app)
self.exclude_paths = exclude_paths or ["/health", "/ready", "/metrics", "/docs", "/redoc"]
self.audit_logger = audit_logger or logger
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Skip capability check for excluded paths
if any(request.url.path.startswith(path) for path in self.exclude_paths):
return await call_next(request)
# Extract and verify token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return Response(
content=format_error(
code=ErrorCode.CAPABILITY_INVALID.value,
message="Missing or invalid authorization header",
capability_used="none",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=401,
media_type="application/json"
)
try:
# Verify token
token = auth_header.replace("Bearer ", "")
verifier = get_verifier()
capability_token = verifier.verify_token(token)
# Store in request state for access by handlers
request.state.capability_token = capability_token
request.state.tenant_id = capability_token.tenant_id
request.state.user_email = capability_token.sub
# Check signature if provided
signature = request.headers.get("X-Capability-Signature")
if signature and not verifier.verify_signature(token, signature):
return Response(
content=format_error(
code=ErrorCode.CAPABILITY_SIGNATURE_INVALID.value,
message="Invalid capability signature",
capability_used="none",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=401,
media_type="application/json"
)
# Process request
response = await call_next(request)
# Audit log successful capability usage
self.audit_logger.info(
f"Capability used: {request.method} {request.url.path}",
extra={
"request_id": getattr(request.state, "request_id", "unknown"),
"tenant_id": capability_token.tenant_id,
"user": capability_token.sub,
"method": request.method,
"path": request.url.path,
"status": response.status_code
}
)
return response
except Exception as e:
logger.error(f"Capability verification failed: {e}")
return Response(
content=format_error(
code=ErrorCode.CAPABILITY_INVALID.value,
message=str(e),
capability_used="none",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=401,
media_type="application/json"
)
class TenantIsolationMiddleware(BaseHTTPMiddleware):
"""
Ensure perfect tenant isolation for all requests
GT 2.0 Philosophy: Security through architectural design
"""
def __init__(
self,
app: ASGIApp,
enforce_isolation: bool = True
):
super().__init__(app)
self.enforce_isolation = enforce_isolation
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Skip for non-tenant paths
if not self.enforce_isolation or request.url.path.startswith("/health"):
return await call_next(request)
# Get tenant from capability token (set by CapabilityMiddleware)
capability_token: Optional[CapabilityToken] = getattr(request.state, "capability_token", None)
if not capability_token:
return await call_next(request) # Let other middleware handle auth
# Extract tenant from path if present
path_parts = request.url.path.strip("/").split("/")
path_tenant = None
# Look for tenant ID in common patterns
if "tenants" in path_parts:
idx = path_parts.index("tenants")
if idx + 1 < len(path_parts):
path_tenant = path_parts[idx + 1]
# Check tenant isolation
if path_tenant and capability_token.tenant_id != path_tenant:
# Only super_admin and gt_admin can access other tenants
if capability_token.user_type not in ["super_admin", "gt_admin"]:
logger.warning(
f"Tenant isolation violation attempted",
extra={
"request_id": getattr(request.state, "request_id", "unknown"),
"user_tenant": capability_token.tenant_id,
"requested_tenant": path_tenant,
"user": capability_token.sub,
"path": request.url.path
}
)
return Response(
content=format_error(
code=ErrorCode.TENANT_ISOLATED.value,
message="Cross-tenant access not allowed",
capability_used=f"tenant:{capability_token.tenant_id}:*",
capability_required=f"tenant:{path_tenant}:*",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=403,
media_type="application/json"
)
# Add tenant context to request
request.state.tenant_context = {
"tenant_id": capability_token.tenant_id,
"requested_tenant": path_tenant,
"user_type": capability_token.user_type,
"isolation_enforced": True
}
return await call_next(request)
class FileSystemIsolationMiddleware(BaseHTTPMiddleware):
"""
Ensure file system access is properly isolated per tenant
GT 2.0 Philosophy: File-based databases with perfect isolation
"""
def __init__(
self,
app: ASGIApp,
base_path: str = "/data"
):
super().__init__(app)
self.base_path = base_path
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Get tenant from request state
tenant_id = getattr(request.state, "tenant_id", None)
if tenant_id:
# Set allowed file paths for this tenant
request.state.allowed_paths = [
f"{self.base_path}/{tenant_id}",
f"{self.base_path}/shared" # Shared read-only resources
]
# Set file encryption key reference
request.state.encryption_key_id = f"{tenant_id}-key-{time.strftime('%Y-%m')}"
return await call_next(request)
class RateLimitMiddleware(BaseHTTPMiddleware):
"""
Simple rate limiting per capability
GT 2.0 Philosophy: Resource protection through simple limits
"""
def __init__(
self,
app: ASGIApp,
requests_per_minute: int = 60,
cache: Optional[Dict[str, list[float]]] = None
):
super().__init__(app)
self.requests_per_minute = requests_per_minute
self.cache = cache or {} # In production, use Redis
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Skip rate limiting for health checks
if request.url.path.startswith("/health"):
return await call_next(request)
# Get user identifier from capability token
capability_token: Optional[CapabilityToken] = getattr(request.state, "capability_token", None)
if not capability_token:
return await call_next(request)
# Create rate limit key
key = f"{capability_token.tenant_id}:{capability_token.sub}"
# Check rate limit
now = time.time()
minute_ago = now - 60
# Get request times for this key
request_times = self.cache.get(key, [])
# Remove old entries
request_times = [t for t in request_times if t > minute_ago]
# Check if limit exceeded
if len(request_times) >= self.requests_per_minute:
return Response(
content=format_error(
code=ErrorCode.RESOURCE_QUOTA_EXCEEDED.value,
message=f"Rate limit exceeded: {self.requests_per_minute} requests per minute",
capability_used=f"tenant:{capability_token.tenant_id}:*",
details={"retry_after": 60 - (now - request_times[0])},
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=429,
media_type="application/json",
headers={"Retry-After": str(int(60 - (now - request_times[0])))}
)
# Add current request time
request_times.append(now)
self.cache[key] = request_times
# Add rate limit headers to response
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
response.headers["X-RateLimit-Remaining"] = str(self.requests_per_minute - len(request_times))
response.headers["X-RateLimit-Reset"] = str(int(minute_ago + 60))
return response

View File

@@ -0,0 +1,161 @@
"""
Standard response formatting for GT 2.0 CB-REST API
Design principle: Simple, consistent, secure responses with built-in audit trail
"""
from typing import Any, Optional, Dict
from datetime import datetime
import uuid
from pydantic import BaseModel, Field
class StandardError(BaseModel):
"""Standard error structure"""
code: str = Field(..., description="Error code (e.g., CAPABILITY_INSUFFICIENT)")
message: str = Field(..., description="Human-readable error message")
capability_required: Optional[str] = Field(None, description="Required capability for this operation")
capability_provided: Optional[str] = Field(None, description="Capability that was provided")
details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")
class StandardResponse(BaseModel):
"""
Standard response format for all CB-REST endpoints
Philosophy: Every response has the same structure, reducing ambiguity
"""
data: Optional[Any] = Field(None, description="Response data (null on error)")
error: Optional[StandardError] = Field(None, description="Error information (null on success)")
capability_used: str = Field(..., description="Capability that authorized this request")
request_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique request identifier")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
def format_response(
data: Any,
capability_used: str,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Format a successful response
Args:
data: The response data
capability_used: The capability that authorized this request
request_id: Optional request ID (will be generated if not provided)
Returns:
Standardized response dictionary
"""
return StandardResponse(
data=data,
error=None,
capability_used=capability_used,
request_id=request_id or str(uuid.uuid4())
).dict(exclude_none=True)
def format_error(
code: str,
message: str,
capability_used: str = "none",
capability_required: Optional[str] = None,
capability_provided: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Format an error response
Args:
code: Error code (e.g., CAPABILITY_INSUFFICIENT)
message: Human-readable error message
capability_used: The capability check that failed
capability_required: The required capability
capability_provided: The capability that was provided
details: Additional error details
request_id: Optional request ID
Returns:
Standardized error response dictionary
"""
error = StandardError(
code=code,
message=message,
capability_required=capability_required,
capability_provided=capability_provided,
details=details
)
return StandardResponse(
data=None,
error=error,
capability_used=capability_used,
request_id=request_id or str(uuid.uuid4())
).dict(exclude_none=True)
class BulkOperationResult(BaseModel):
"""Result of a single operation in a bulk request"""
operation_id: str = Field(..., description="Unique identifier for this operation")
action: str = Field(..., description="Action performed (create, update, delete)")
resource_id: Optional[str] = Field(None, description="ID of the affected resource")
success: bool = Field(..., description="Whether the operation succeeded")
error: Optional[StandardError] = Field(None, description="Error if operation failed")
data: Optional[Any] = Field(None, description="Result data if operation succeeded")
class BulkResponse(BaseModel):
"""Response format for bulk operations"""
operations: list[BulkOperationResult] = Field(..., description="Results of all operations")
transaction: bool = Field(..., description="Whether all operations were in a transaction")
total: int = Field(..., description="Total number of operations")
succeeded: int = Field(..., description="Number of successful operations")
failed: int = Field(..., description="Number of failed operations")
capability_used: str = Field(..., description="Capability that authorized this bulk operation")
request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
def format_bulk_response(
operations: list[BulkOperationResult],
transaction: bool,
capability_used: str,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Format a bulk operation response
Args:
operations: List of operation results
transaction: Whether operations were transactional
capability_used: The capability that authorized this request
request_id: Optional request ID
Returns:
Standardized bulk response dictionary
"""
succeeded = sum(1 for op in operations if op.success)
failed = len(operations) - succeeded
response = BulkResponse(
operations=operations,
transaction=transaction,
total=len(operations),
succeeded=succeeded,
failed=failed,
capability_used=capability_used,
request_id=request_id or str(uuid.uuid4())
)
# For bulk responses, we wrap in standard response
return StandardResponse(
data=response.dict(exclude_none=True),
error=None,
capability_used=capability_used,
request_id=response.request_id
).dict(exclude_none=True)