Files
taylanbakircioglu d7ca50b387 feat: Flowfish - eBPF-powered multi-cluster Kubernetes observability platform
Multi-cluster dependency mapping, real-time network monitoring,
impact analysis, and CI/CD integration capabilities.

Made-with: Cursor
2026-03-29 21:43:20 +03:00

352 lines
11 KiB
Python

"""
JWT token utilities for authentication
Supports two authentication methods:
1. Bearer token (JWT) - for UI users
2. X-API-Key header - for CI/CD pipelines
"""
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
import jwt
import hashlib
from fastapi import Depends, HTTPException, status, Request, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import structlog
from config import settings
from database.postgresql import database
logger = structlog.get_logger()
# HTTP Bearer token extractor (auto_error=False to allow API key fallback)
security = HTTPBearer(auto_error=False)
def create_access_token(data: Dict[str, Any]) -> str:
"""Create JWT access token"""
try:
to_encode = data.copy()
# Set expiration
expire = datetime.utcnow() + timedelta(hours=settings.JWT_EXPIRATION_HOURS)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "access"
})
# Encode token
encoded_jwt = jwt.encode(
to_encode,
settings.SECRET_KEY,
algorithm=settings.JWT_ALGORITHM
)
return encoded_jwt
except Exception as e:
logger.error("Failed to create access token", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token creation failed"
)
def verify_token(token: str) -> Dict[str, Any]:
"""Verify and decode JWT token"""
try:
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM]
)
# Check token type
if payload.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type"
)
# Check if token is expired
exp = payload.get("exp")
if exp and datetime.utcfromtimestamp(exp) < datetime.utcnow():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
# TODO: Check token blacklist
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.InvalidTokenError as e:
logger.warning("Invalid token", error=str(e))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
except Exception as e:
logger.error("Token verification failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token verification failed"
)
async def verify_api_key(api_key: str, client_ip: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Verify an API key and return user info if valid.
Returns None if invalid.
"""
if not api_key or not api_key.startswith("fk_"):
return None
try:
# Hash the provided key
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
# Look up in database
query = """
SELECT
ak.id as key_id, ak.key_id as key_identifier, ak.name as key_name,
ak.scopes, ak.cluster_ids, ak.user_id,
u.username, u.email
FROM api_keys ak
JOIN users u ON ak.user_id = u.id
WHERE ak.key_hash = :key_hash
AND ak.is_active = TRUE
AND (ak.expires_at IS NULL OR ak.expires_at > NOW())
"""
result = await database.fetch_one(query, {"key_hash": key_hash})
if result:
# Update usage statistics
update_query = """
UPDATE api_keys
SET last_used_at = NOW(),
usage_count = usage_count + 1,
last_used_ip = :client_ip
WHERE id = :id
"""
await database.execute(update_query, {"id": result["key_id"], "client_ip": client_ip})
logger.info(
"API key authenticated",
key_id=result["key_identifier"],
username=result["username"]
)
return {
"id": result["user_id"],
"user_id": result["user_id"],
"username": result["username"],
"email": result["email"],
"roles": ["API Key User"], # API keys have limited role
"auth_type": "api_key",
"key_id": result["key_identifier"],
"key_name": result["key_name"],
"scopes": result["scopes"] or [],
"cluster_ids": result["cluster_ids"]
}
return None
except Exception as e:
logger.error("API key verification failed", error=str(e))
return None
async def get_current_user(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
) -> Dict[str, Any]:
"""
FastAPI dependency to get current authenticated user.
Supports two authentication methods:
1. Bearer token (JWT) - Authorization: Bearer <token>
2. API Key - X-API-Key: fk_xxx...
"""
# Get client IP for logging
client_ip = request.client.host if request.client else None
if request.headers.get("X-Forwarded-For"):
client_ip = request.headers.get("X-Forwarded-For").split(",")[0].strip()
# Method 1: Try API Key first (X-API-Key header)
if x_api_key:
api_key_user = await verify_api_key(x_api_key, client_ip)
if api_key_user:
return api_key_user
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired API key"
)
# Method 2: Try Bearer token (JWT)
if credentials:
try:
token = credentials.credentials
# Verify token and get payload
payload = verify_token(token)
# Extract user information
user_id = payload.get("user_id")
username = payload.get("username")
roles = payload.get("roles", [])
if not user_id or not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
return {
"id": user_id,
"user_id": user_id,
"username": username,
"roles": roles,
"auth_type": "jwt",
"token_payload": payload
}
except HTTPException:
raise
except Exception as e:
logger.error("JWT verification failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# No authentication provided
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated. Provide either 'Authorization: Bearer <token>' or 'X-API-Key: <key>'"
)
async def get_optional_user(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
) -> Optional[Dict[str, Any]]:
"""FastAPI dependency to get current user if authenticated (optional)"""
if not credentials and not x_api_key:
return None
try:
return await get_current_user(request, credentials, x_api_key)
except HTTPException:
return None
def require_roles(*required_roles: str):
"""Decorator to require specific roles"""
def decorator(current_user: Dict[str, Any] = Depends(get_current_user)):
user_roles = current_user.get("roles", [])
# Super Admin has access to everything
if "Super Admin" in user_roles:
return current_user
# Check if user has any of the required roles
if not any(role in user_roles for role in required_roles):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Requires one of the following roles: {', '.join(required_roles)}"
)
return current_user
return decorator
def require_permissions(*required_permissions: str):
"""Decorator to require specific permissions"""
async def decorator(current_user: Dict[str, Any] = Depends(get_current_user)):
user_id = current_user["user_id"]
# Check permissions from database
permissions_query = """
SELECT DISTINCT p.resource, p.action
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN roles r ON rp.role_id = r.id
JOIN user_roles ur ON r.id = ur.role_id
WHERE ur.user_id = :user_id
"""
user_permissions = await database.fetch_all(permissions_query, {"user_id": user_id})
user_permission_set = {
f"{perm['resource']}.{perm['action']}"
for perm in user_permissions
}
# Check if user has required permissions
missing_permissions = []
for permission in required_permissions:
if permission not in user_permission_set:
missing_permissions.append(permission)
if missing_permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing permissions: {', '.join(missing_permissions)}"
)
return current_user
return decorator
# Helper functions
async def authenticate_user(username: str, password: str) -> Optional[Dict[str, Any]]:
"""Authenticate user with username and password"""
try:
query = """
SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name,
u.is_active, u.is_locked,
array_agg(r.name) as roles
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.username = :username AND u.is_active = true
GROUP BY u.id
"""
user_record = await database.fetch_one(query, {"username": username})
if not user_record or user_record["is_locked"]:
return None
# TODO: Implement proper password verification with bcrypt
# For MVP, simplified check
if user_record["password_hash"]:
# Verify password here
pass
return {
"id": user_record["id"],
"username": user_record["username"],
"email": user_record["email"],
"full_name": f"{user_record['first_name'] or ''} {user_record['last_name'] or ''}".strip() or user_record["username"],
"roles": user_record["roles"] or [],
"is_active": user_record["is_active"]
}
except Exception as e:
logger.error("User authentication failed", error=str(e))
return None