GT AI OS Community v2.0.33 - Add NVIDIA NIM and Nemotron agents

- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2
- Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2
  - Made more general-purpose (flexible targets, expanded tools)
- Added nemotron-mini-agent.csv for fast local inference via Ollama
- Added nemotron-agent.csv for advanced reasoning via Ollama
- Added wiki page: Projects for NVIDIA NIMs and Nemotron
This commit is contained in:
HackWeasel
2025-12-12 17:47:14 -05:00
commit 310491a557
750 changed files with 232701 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
"""
GT 2.0 Tenant Backend Middleware
Security and isolation middleware for tenant applications.
"""

View File

@@ -0,0 +1,385 @@
"""
OAuth2 Authentication Middleware for GT 2.0 Tenant Backend
Handles OAuth2 authentication headers from OAuth2 Proxy and extracts
user information for tenant isolation and access control.
"""
from fastapi import Request, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Optional, Dict, Any
import logging
import json
import base64
from urllib.parse import unquote
logger = logging.getLogger(__name__)
class OAuth2AuthMiddleware(BaseHTTPMiddleware):
"""
Middleware to handle OAuth2 authentication from OAuth2 Proxy.
Extracts user information from OAuth2 Proxy headers and sets
user context for downstream handlers.
"""
# Routes that don't require authentication
EXEMPT_PATHS = {
"/health",
"/metrics",
"/docs",
"/openapi.json",
"/api/v1/health",
"/api/v1/auth/login",
"/api/v1/auth/refresh",
"/api/v1/auth/logout"
}
def __init__(self, app, require_auth: bool = True):
super().__init__(app)
self.require_auth = require_auth
async def dispatch(self, request: Request, call_next):
"""Process OAuth2 authentication headers"""
# Skip authentication for exempt paths
if request.url.path in self.EXEMPT_PATHS:
return await call_next(request)
# Try OAuth2 headers first, then fallback to JWT token authentication
user_info = self._extract_oauth2_headers(request)
# If no OAuth2 headers found, try JWT token authentication
if not user_info:
user_info = await self._extract_jwt_user(request)
if self.require_auth and not user_info:
logger.warning(f"Authentication required but no valid OAuth2 headers found for {request.url.path}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"}
)
# Set user context in request state
if user_info:
request.state.user = user_info
request.state.authenticated = True
logger.info(f"Authenticated user: {user_info.get('email', 'unknown')} for {request.url.path}")
else:
request.state.user = None
request.state.authenticated = False
# Continue with request processing
response = await call_next(request)
# Add authentication-related headers to response
if user_info:
response.headers["X-Authenticated-User"] = user_info.get("email", "unknown")
response.headers["X-Auth-Source"] = user_info.get("auth_source", "oauth2-proxy")
return response
def _extract_oauth2_headers(self, request: Request) -> Optional[Dict[str, Any]]:
"""
Extract user information from OAuth2 Proxy headers.
OAuth2 Proxy sets the following headers:
- X-Auth-Request-User: Username/email
- X-Auth-Request-Email: User email
- X-Auth-Request-Access-Token: Access token
- Authorization: Bearer token (if configured)
"""
# Extract user information from OAuth2 Proxy headers
user_email = request.headers.get("X-Auth-Request-Email")
user_name = request.headers.get("X-Auth-Request-User")
access_token = request.headers.get("X-Auth-Request-Access-Token")
# Also check Authorization header for bearer token
auth_header = request.headers.get("Authorization")
bearer_token = None
if auth_header and auth_header.startswith("Bearer "):
bearer_token = auth_header[7:] # Remove "Bearer " prefix
if not user_email and not user_name:
logger.debug("No OAuth2 authentication headers found")
return None
user_info = {
"email": user_email,
"username": user_name or user_email,
"access_token": access_token,
"bearer_token": bearer_token,
"auth_source": "oauth2-proxy",
"authenticated_at": request.headers.get("X-Auth-Request-Timestamp"),
}
# Extract additional user attributes if present
if groups_header := request.headers.get("X-Auth-Request-Groups"):
try:
# Groups might be base64 encoded or comma-separated
if self._is_base64(groups_header):
groups_decoded = base64.b64decode(groups_header).decode('utf-8')
user_info["groups"] = json.loads(groups_decoded)
else:
user_info["groups"] = groups_header.split(",")
except (json.JSONDecodeError, UnicodeDecodeError) as e:
logger.warning(f"Failed to decode groups header: {e}")
user_info["groups"] = []
# Extract user roles if present
if roles_header := request.headers.get("X-Auth-Request-Roles"):
try:
if self._is_base64(roles_header):
roles_decoded = base64.b64decode(roles_header).decode('utf-8')
user_info["roles"] = json.loads(roles_decoded)
else:
user_info["roles"] = roles_header.split(",")
except (json.JSONDecodeError, UnicodeDecodeError) as e:
logger.warning(f"Failed to decode roles header: {e}")
user_info["roles"] = []
# Extract tenant information from headers or JWT token
tenant_id = self._extract_tenant_info(request, user_info)
if tenant_id:
user_info["tenant_id"] = tenant_id
return user_info
def _extract_tenant_info(self, request: Request, user_info: Dict[str, Any]) -> Optional[str]:
"""
Extract tenant information from request headers or JWT token.
Tenant information can come from:
1. X-Tenant-ID header (set by load balancer based on domain)
2. JWT token claims
3. Domain name parsing
"""
# Check for explicit tenant header
if tenant_header := request.headers.get("X-Tenant-ID"):
return tenant_header
# Extract tenant from domain name
host = request.headers.get("Host", "")
if host and "." in host:
# Assume format: tenant.gt2.com
potential_tenant = host.split(".")[0]
if potential_tenant != "www" and potential_tenant != "api":
return potential_tenant
# Try to extract from JWT token if present
if bearer_token := user_info.get("bearer_token"):
tenant_from_jwt = self._extract_tenant_from_jwt(bearer_token)
if tenant_from_jwt:
return tenant_from_jwt
logger.warning(f"Could not determine tenant for user {user_info.get('email', 'unknown')}")
return None
def _extract_tenant_from_jwt(self, token: str) -> Optional[str]:
"""
Extract tenant information from JWT token without verifying signature.
Note: This is just for extracting claims, not for security validation.
Security validation should be done by OAuth2 Proxy.
"""
try:
# Split JWT token (header.payload.signature)
parts = token.split(".")
if len(parts) != 3:
return None
# Decode payload (add padding if needed)
payload = parts[1]
# Add padding if needed for base64 decoding
payload += "=" * (4 - len(payload) % 4)
decoded_payload = base64.urlsafe_b64decode(payload)
claims = json.loads(decoded_payload)
# Look for tenant in various claim fields
tenant_claims = ["tenant_id", "tenant", "org_id", "organization"]
for claim in tenant_claims:
if claim in claims:
return str(claims[claim])
except (json.JSONDecodeError, UnicodeDecodeError, ValueError) as e:
logger.debug(f"Failed to decode JWT payload: {e}")
return None
def _is_base64(self, s: str) -> bool:
"""Check if a string is base64 encoded"""
try:
if isinstance(s, str):
s = s.encode('ascii')
return base64.b64encode(base64.b64decode(s)) == s
except Exception:
return False
async def _extract_jwt_user(self, request: Request) -> Optional[Dict[str, Any]]:
"""
Extract user information from JWT token in Authorization header.
This provides fallback authentication when OAuth2 proxy headers are not present.
"""
from app.core.security import get_current_user
try:
# Get Authorization header
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
# Use the security module to validate and extract user info
user_data = await get_current_user(auth_header)
# Convert security module format to middleware format
if user_data:
return {
"email": user_data.get("email", user_data.get("user_id", "unknown")),
"username": user_data.get("tenant_display_name", user_data.get("email", "unknown")),
"tenant_id": user_data.get("tenant_id", "1"),
"tenant_domain": user_data.get("tenant_domain", "default"),
"tenant_name": user_data.get("tenant_name", "Default Tenant"),
"tenant_role": user_data.get("tenant_role", "tenant_user"),
"user_type": user_data.get("user_type", "tenant_user"),
"capabilities": user_data.get("capabilities", []),
"resource_limits": user_data.get("resource_limits", {}),
"auth_source": "jwt-token",
"bearer_token": auth_header[7:], # Remove "Bearer " prefix
"authenticated_at": None,
"is_primary_tenant": user_data.get("is_primary_tenant", False)
}
except Exception as e:
logger.debug(f"Failed to authenticate via JWT token: {e}")
return None
return None
class OAuth2SecurityDependency:
"""
FastAPI dependency to get current authenticated user from OAuth2 context.
Usage:
@app.get("/api/v1/user/profile")
async def get_profile(user: dict = Depends(get_current_user)):
return {"user": user}
"""
def __call__(self, request: Request) -> Dict[str, Any]:
"""Get current authenticated user from request state"""
if not hasattr(request.state, "authenticated") or not request.state.authenticated:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"}
)
return request.state.user
# Singleton instance for dependency injection
get_current_user = OAuth2SecurityDependency()
def get_current_user_optional(request: Request) -> Optional[Dict[str, Any]]:
"""
Get current authenticated user (optional - doesn't raise exception if not authenticated).
Usage:
@app.get("/api/v1/public/info")
async def get_info(user: Optional[dict] = Depends(get_current_user_optional)):
if user:
return {"message": f"Hello {user['email']}"}
return {"message": "Hello anonymous user"}
"""
if hasattr(request.state, "authenticated") and request.state.authenticated:
return request.state.user
return None
def require_tenant_access(required_tenant: Optional[str] = None):
"""
Dependency to ensure user has access to specified tenant.
Usage:
@app.get("/api/v1/tenant/{tenant_id}/data")
async def get_tenant_data(
tenant_id: str,
user: dict = Depends(get_current_user),
_: None = Depends(require_tenant_access)
):
# User is guaranteed to have access to tenant_id
return {"data": "tenant specific data"}
"""
def dependency(request: Request, user: Dict[str, Any] = Depends(get_current_user)) -> None:
"""Check tenant access for current user"""
user_tenant = user.get("tenant_id")
# If no required tenant specified, use the one from user context
target_tenant = required_tenant or user_tenant
if not target_tenant:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant information not available"
)
# Check if user has access to the required tenant
if user_tenant != target_tenant:
logger.warning(
f"User {user.get('email', 'unknown')} attempted to access tenant {target_tenant} "
f"but belongs to tenant {user_tenant}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: insufficient tenant permissions"
)
return dependency
def require_roles(*required_roles: str):
"""
Dependency to ensure user has one of the required roles.
Usage:
@app.delete("/api/v1/admin/users/{user_id}")
async def delete_user(
user_id: str,
user: dict = Depends(get_current_user),
_: None = Depends(require_roles("admin", "user_manager"))
):
# User has admin or user_manager role
return {"deleted": user_id}
"""
def dependency(user: Dict[str, Any] = Depends(get_current_user)) -> None:
"""Check role requirements for current user"""
user_roles = set(user.get("roles", []))
required_roles_set = set(required_roles)
if not user_roles.intersection(required_roles_set):
logger.warning(
f"User {user.get('email', 'unknown')} with roles {list(user_roles)} "
f"attempted to access endpoint requiring roles {list(required_roles_set)}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Access denied: requires one of roles: {', '.join(required_roles)}"
)
return dependency

View File

@@ -0,0 +1,89 @@
"""
Rate Limiting Middleware for GT 2.0
Basic rate limiting implementation for tenant protection.
"""
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
import time
from typing import Dict, Tuple
import logging
from app.core.config import get_settings
logger = logging.getLogger(__name__)
settings = get_settings()
class RateLimitMiddleware(BaseHTTPMiddleware):
"""Simple in-memory rate limiting middleware"""
# Operational endpoints that don't need rate limiting
EXEMPT_PATHS = {
"/health",
"/ready",
"/metrics",
"/api/v1/health"
}
def __init__(self, app):
super().__init__(app)
self._rate_limits: Dict[str, Tuple[int, float]] = {} # ip -> (count, window_start)
async def dispatch(self, request: Request, call_next):
# Skip rate limiting for operational endpoints
if request.url.path in self.EXEMPT_PATHS:
return await call_next(request)
client_ip = self._get_client_ip(request)
if self._is_rate_limited(client_ip):
logger.warning(f"Rate limit exceeded for IP: {client_ip} - Path: {request.url.path}")
# Return proper JSONResponse instead of raising HTTPException to prevent ASGI violations
return JSONResponse(
status_code=429,
content={"detail": "Too many requests. Please try again later."},
headers={"Retry-After": str(settings.rate_limit_window_seconds)}
)
response = await call_next(request)
return response
def _get_client_ip(self, request: Request) -> str:
"""Extract client IP address"""
# Check for forwarded IP first (behind proxy/load balancer)
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
# Check for real IP header
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
# Fall back to direct client IP
return request.client.host if request.client else "unknown"
def _is_rate_limited(self, client_ip: str) -> bool:
"""Check if client IP is rate limited"""
current_time = time.time()
if client_ip not in self._rate_limits:
self._rate_limits[client_ip] = (1, current_time)
return False
count, window_start = self._rate_limits[client_ip]
# Check if we're still in the same window
if current_time - window_start < settings.rate_limit_window_seconds:
if count >= settings.rate_limit_requests:
return True # Rate limited
else:
self._rate_limits[client_ip] = (count + 1, window_start)
return False
else:
# New window, reset count
self._rate_limits[client_ip] = (1, current_time)
return False

View File

@@ -0,0 +1,36 @@
"""
Security Headers Middleware for GT 2.0
Adds security headers to all responses.
"""
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
import uuid
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Middleware to add security headers to all responses"""
async def dispatch(self, request: Request, call_next):
# Generate request ID for tracing
request_id = str(uuid.uuid4())
request.state.request_id = request_id
response = await call_next(request)
# Add security headers
response.headers["X-Request-ID"] = request_id
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"connect-src 'self' ws: wss:;"
)
return response

View File

@@ -0,0 +1,156 @@
"""
GT 2.0 Session Validation Middleware
OWASP/NIST Compliant Server-Side Session Validation (Issue #264)
- Validates session_id from JWT against server-side session state
- Updates session activity on every authenticated request
- Adds X-Session-Warning header when < 5 minutes remaining
- Returns 401 with X-Session-Expired header when session is invalid
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
import httpx
import logging
import jwt
from app.core.config import get_settings
settings = get_settings()
logger = logging.getLogger(__name__)
class SessionValidationMiddleware(BaseHTTPMiddleware):
"""
Middleware to validate server-side sessions on every authenticated request.
The server-side session is the authoritative source of truth for session validity.
JWT expiration is secondary - the session can expire before the JWT does.
Response Headers:
- X-Session-Warning: <seconds> - Added when session is about to expire
- X-Session-Expired: idle|absolute - Added on 401 when session expired
"""
def __init__(self, app, control_panel_url: str = None, service_auth_token: str = None):
super().__init__(app)
self.control_panel_url = control_panel_url or settings.control_panel_url or "http://control-panel-backend:8001"
self.service_auth_token = service_auth_token or settings.service_auth_token or "internal-service-token"
async def dispatch(self, request: Request, call_next):
"""Process request and validate server-side session"""
# Skip session validation for public endpoints
skip_paths = [
"/health",
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/refresh",
"/api/v1/auth/password-reset",
"/api/v1/public",
"/docs",
"/openapi.json",
"/redoc"
]
if any(request.url.path.startswith(path) for path in skip_paths):
return await call_next(request)
# Extract JWT from Authorization header
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return await call_next(request)
token = auth_header.split(" ")[1]
# Decode JWT to get session_id (without verification - that's done elsewhere)
try:
# We just need to extract the session_id claim
# Full JWT verification happens in the auth dependency
payload = jwt.decode(token, options={"verify_signature": False})
session_id = payload.get("session_id")
except jwt.InvalidTokenError:
# Let the normal auth flow handle invalid tokens
return await call_next(request)
# If no session_id in JWT, skip session validation (backwards compatibility)
# This allows old tokens without session_id to work until they expire
if not session_id:
logger.debug("No session_id in JWT, skipping server-side validation")
return await call_next(request)
# Validate session with control panel
validation_result = await self._validate_session(session_id)
if validation_result is None:
# Control panel unavailable - FAIL CLOSED for security (OWASP best practice)
# Reject the request rather than allowing potentially expired sessions through
logger.error("Session validation failed - control panel unavailable, rejecting request")
return JSONResponse(
status_code=503,
content={
"detail": "Session validation service unavailable",
"code": "SESSION_VALIDATION_UNAVAILABLE"
},
headers={"X-Session-Warning": "validation-unavailable"}
)
if not validation_result.get("is_valid", False):
# Session is invalid - return 401 with expiry reason
# Ensure expiry_reason is never None (causes header encode error)
expiry_reason = validation_result.get("expiry_reason") or "unknown"
logger.info(f"Session expired: {expiry_reason}")
return JSONResponse(
status_code=401,
content={
"detail": f"Session expired ({expiry_reason})",
"code": "SESSION_EXPIRED",
"expiry_reason": expiry_reason
},
headers={"X-Session-Expired": expiry_reason}
)
# Session is valid - process request
response = await call_next(request)
# Add warning header if session is about to expire
if validation_result.get("show_warning", False):
seconds_remaining = validation_result.get("seconds_remaining", 0)
response.headers["X-Session-Warning"] = str(seconds_remaining)
logger.debug(f"Session warning: {seconds_remaining}s remaining")
return response
async def _validate_session(self, session_token: str) -> dict | None:
"""
Validate session with control panel internal API.
Returns:
dict with is_valid, expiry_reason, seconds_remaining, show_warning
or None if control panel is unavailable
"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.post(
f"{self.control_panel_url}/internal/sessions/validate",
json={"session_token": session_token},
headers={
"X-Service-Auth": self.service_auth_token,
"X-Service-Name": "tenant-backend"
}
)
if response.status_code == 200:
return response.json()
else:
logger.error(f"Session validation failed: {response.status_code} - {response.text}")
return None
except httpx.RequestError as e:
logger.error(f"Session validation request failed: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error during session validation: {e}")
return None

View File

@@ -0,0 +1,48 @@
"""
Tenant Isolation Middleware for GT 2.0
Ensures perfect tenant isolation for all requests.
"""
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
import logging
from app.core.config import get_settings
logger = logging.getLogger(__name__)
settings = get_settings()
class TenantIsolationMiddleware(BaseHTTPMiddleware):
"""Middleware to enforce tenant isolation boundaries"""
async def dispatch(self, request: Request, call_next):
# Add tenant context to request
request.state.tenant_id = settings.tenant_id
request.state.tenant_domain = settings.tenant_domain
# Validate tenant isolation
await self._validate_tenant_isolation(request)
response = await call_next(request)
# Add tenant headers to response
response.headers["X-Tenant-Domain"] = settings.tenant_domain
response.headers["X-Tenant-Isolated"] = "true"
return response
async def _validate_tenant_isolation(self, request: Request):
"""Validate that all operations are tenant-isolated"""
# This is where we would add tenant boundary validation
# For now, we just log the tenant context
logger.debug(
"Tenant isolation validated",
extra={
"tenant_id": settings.tenant_id,
"tenant_domain": settings.tenant_domain,
"path": request.url.path,
"method": request.method,
}
)