Spaces:
Running
Running
| """ | |
| API Key Management and RBAC. | |
| Raw keys are never stored — only SHA-256 hash is persisted. | |
| Storage: api_keys.jsonl (append-only) | |
| Roles: | |
| viewer - GET only | |
| analyst - GET + POST (all analysis endpoints) | |
| admin - all methods including key management | |
| """ | |
| import json | |
| import uuid | |
| import hashlib | |
| import secrets | |
| import logging | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Dict, Any, Optional, List | |
| logger = logging.getLogger(__name__) | |
| import threading as _threading | |
| _key_write_lock = _threading.Lock() | |
| KEYS_PATH = Path(__file__).parent.parent / "data" / "api_keys.jsonl" | |
| ROLES = { | |
| "viewer": {"GET"}, | |
| "analyst": {"GET", "POST"}, | |
| "admin": {"GET", "POST", "PATCH", "DELETE"}, | |
| } | |
| _ANALYST_PATHS = {"/api/v1/analyze/", "/api/v1/cases/"} | |
| _ADMIN_PATHS = {"/api/v1/keys/"} | |
| def _now() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| def _hash_key(raw_key: str) -> str: | |
| return hashlib.sha256(raw_key.encode("utf-8")).hexdigest() | |
| def _load_keys() -> Dict[str, Dict[str, Any]]: | |
| keys: Dict[str, Dict[str, Any]] = {} | |
| if not KEYS_PATH.exists(): | |
| return keys | |
| try: | |
| for line in KEYS_PATH.read_text(encoding="utf-8").splitlines(): | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| entry = json.loads(line) | |
| kid = entry.get("key_id") | |
| if kid: | |
| keys[kid] = entry | |
| except json.JSONDecodeError: | |
| continue | |
| except Exception as e: | |
| logger.error(f"Failed to load API keys: {e}") | |
| return keys | |
| def _save_key(entry: Dict[str, Any]) -> None: | |
| try: | |
| with open(KEYS_PATH, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(entry) + "\n") | |
| except Exception as e: | |
| logger.error(f"Failed to save API key: {e}") | |
| def create_key(name: str, role: str = "analyst", | |
| description: str = "", created_by: str = "system") -> Dict[str, Any]: | |
| if role not in ROLES: | |
| return {"error": f"Invalid role. Must be one of: {list(ROLES.keys())}"} | |
| if not name or not name.strip(): | |
| return {"error": "Key name is required."} | |
| raw_key = f"vfx_{secrets.token_urlsafe(32)}" | |
| key_hash = _hash_key(raw_key) | |
| key_id = str(uuid.uuid4()) | |
| entry = { | |
| "key_id": key_id, "name": name.strip()[:100], | |
| "description": description.strip()[:500], "role": role, | |
| "key_hash": key_hash, "created_at": _now(), "created_by": created_by, | |
| "last_used": None, "use_count": 0, "active": True, | |
| } | |
| _save_key(entry) | |
| logger.info(f"API key created: {key_id} name={name} role={role}") | |
| return {**entry, "key": raw_key, | |
| "warning": "Save this key now. It will not be shown again.", | |
| "key_hash": "[hidden]"} | |
| def verify_key(raw_key: str) -> Optional[Dict[str, Any]]: | |
| if not raw_key or not raw_key.startswith("vfx_"): | |
| return None | |
| key_hash = _hash_key(raw_key) | |
| keys = _load_keys() | |
| for entry in keys.values(): | |
| if entry.get("key_hash") == key_hash and entry.get("active", False): | |
| entry["last_used"] = _now() | |
| entry["use_count"] = entry.get("use_count", 0) + 1 | |
| with _key_write_lock: | |
| _save_key(entry) | |
| return {k: v for k, v in entry.items() if k != "key_hash"} | |
| return None | |
| def check_permission(role: str, method: str, path: str) -> bool: | |
| allowed_methods = ROLES.get(role, set()) | |
| if method.upper() not in allowed_methods: | |
| return False | |
| if role == "viewer": | |
| for restricted in list(_ANALYST_PATHS) + list(_ADMIN_PATHS): | |
| if path.startswith(restricted): | |
| return False | |
| if role == "analyst": | |
| for restricted in _ADMIN_PATHS: | |
| if path.startswith(restricted): | |
| return False | |
| return True | |
| def revoke_key(key_id: str, revoked_by: str = "system") -> Dict[str, Any]: | |
| keys = _load_keys() | |
| if key_id not in keys: | |
| return {"error": f"Key not found: {key_id}"} | |
| entry = keys[key_id] | |
| entry["active"] = False | |
| entry["revoked_at"] = _now() | |
| entry["revoked_by"] = revoked_by | |
| _save_key(entry) | |
| logger.info(f"API key revoked: {key_id}") | |
| return {k: v for k, v in entry.items() if k != "key_hash"} | |
| def list_keys(include_inactive: bool = False) -> List[Dict[str, Any]]: | |
| keys = _load_keys() | |
| result = [] | |
| for entry in keys.values(): | |
| if not include_inactive and not entry.get("active", False): | |
| continue | |
| result.append({k: v for k, v in entry.items() if k != "key_hash"}) | |
| return sorted(result, key=lambda x: x.get("created_at", ""), reverse=True) | |