Spaces:
Runtime error
Runtime error
File size: 6,901 Bytes
62b83b2 f86beae 62b83b2 f86beae 62b83b2 f86beae 62b83b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """Auth, RBAC, and API-key management (the three must-haves).
1. Superadmin login -> a login session token with expiry.
2. User session expiry -> login_sessions.expires_at (LOGIN_SESSION_TTL_HOURS).
3. Provider API keys -> api_keys.expires_at (lifetime or N days, default 2 weeks).
Roles:
- 'user' : end users. scopes = chat + websearch + image (NO agentic tools).
- 'superadmin' : full access (chat, websearch, image, agents, commands, files, mcp).
"""
from __future__ import annotations
import hashlib
import hmac
import re
import secrets
import time
from datetime import datetime, timedelta, timezone
from . import config, db
# ---------- brute-force protection (in-memory, per-instance) ----------
_fails: dict[tuple, list[float]] = {}
MAX_FAILS = 5 # failures allowed per window before lockout
FAIL_WINDOW = 300.0 # 5-minute sliding window
def too_many_fails(ip: str, bucket: str) -> bool:
now = time.time()
k = (ip, bucket)
arr = [t for t in _fails.get(k, []) if now - t < FAIL_WINDOW]
_fails[k] = arr
return len(arr) >= MAX_FAILS
def record_fail(ip: str, bucket: str) -> None:
_fails.setdefault((ip, bucket), []).append(time.time())
def clear_fails(ip: str, bucket: str) -> None:
_fails.pop((ip, bucket), None)
# ---------- input hardening ----------
_SAFE_SID = re.compile(r"[^a-zA-Z0-9_.\-]")
def safe_session_id(sid: str) -> str:
"""Strip anything that could traverse the filesystem; keep it a flat token."""
sid = _SAFE_SID.sub("_", str(sid))[:128]
sid = sid.replace("..", "_")
return sid or "default"
def within(child, parent) -> bool:
"""True only if `child` resolves to a path inside `parent` (containment guarantee)."""
from pathlib import Path
try:
Path(child).resolve().relative_to(Path(parent).resolve())
return True
except (ValueError, OSError):
return False
# ---------- prompt-extraction / jailbreak guard (deterministic, pre-model) ----------
_JAILBREAK = re.compile(
r"(?is)("
r"ignore\s+(?:the\s+|all\s+|any\s+|your\s+|previous\s+|prior\s+|above\s+|earlier\s+)*"
r"(?:instruction|rule|prompt|guideline|directive)s?"
r"|(?:reveal|show|print|repeat|give\s+me|tell\s+me|share|output|display|leak|expose|"
r"what(?:'s| is| are)|state|recite)\b[^.\n]{0,40}\b(?:your|the|its|first|initial|original|hidden|secret)\b"
r"[^.\n]{0,25}\b(?:system\s+)?(?:prompt|instruction|rule|configuration|guideline|directive)s?"
r"|repeat\s+(?:everything|all|the\s+text|what(?:'s| is)?)\b[^.\n]{0,20}\babove"
r"|(?:developer|debug|god|admin|dev|sudo|dan)\s*mode"
r"|\bjailbreak\b|\bsystem\s*prompt\b"
r"|pretend\s+(?:you\s+are|to\s+be)\s+(?:a\s+)?(?:different|another|new)\s+(?:ai|system|model|assistant)"
r")"
)
EXTRACTION_REFUSAL = (
"I can't share my internal configuration or instructions — but I'm happy to help "
"with a question, search, or task."
)
def is_extraction_attempt(text: str) -> bool:
return bool(_JAILBREAK.search(text or ""))
# RBAC: what each role is allowed to do.
ROLE_SCOPES = {
"user": ["chat", "websearch", "image"],
"superadmin": ["chat", "websearch", "image", "agents", "commands", "files", "mcp"],
}
# ---------- password hashing (stdlib pbkdf2, no extra deps) ----------
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000)
return f"pbkdf2$200000${salt}${dk.hex()}"
def verify_password(password: str, stored: str) -> bool:
try:
algo, iters, salt, want = stored.split("$")
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), int(iters))
return hmac.compare_digest(dk.hex(), want)
except Exception:
return False
# ---------- superadmin bootstrap ----------
def ensure_superadmin() -> None:
"""Create/refresh the superadmin from env on startup (no-op without DB or password)."""
if not db.available() or not config.SUPERADMIN_PASSWORD:
return
db.init_schema()
db.upsert_user(config.SUPERADMIN_USER, hash_password(config.SUPERADMIN_PASSWORD), "superadmin")
# ---------- login sessions (with expiry) ----------
def login(username: str, password: str) -> dict | None:
user = db.get_user(username)
if not user or not verify_password(password, user["password_hash"]):
return None
token = "sess_" + secrets.token_urlsafe(32)
expires = datetime.now(timezone.utc) + timedelta(hours=config.LOGIN_SESSION_TTL_HOURS)
db.create_login(token, user["id"], user["role"], expires)
return {"token": token, "role": user["role"], "expires_at": expires.isoformat(),
"user_id": user["id"]}
def check_login(token: str) -> dict | None:
"""Return the login session if valid and not expired, else None."""
if not token:
return None
return db.get_login(token)
# ---------- provider API keys (with expiry + scopes) ----------
def _hash_key(raw: str) -> str:
return hashlib.sha256(raw.encode()).hexdigest()
def generate_api_key(role: str, label: str = "", scopes: list[str] | None = None,
ttl_days: int | None = None, created_by: int | None = None,
max_sessions: int | None = None, agents_md: str | None = None) -> dict:
"""Create a key. Returns the FULL key once (store it; only the hash is persisted).
ttl_days: None -> default; 0 -> lifetime. max_sessions: None/0 -> unlimited.
agents_md: optional per-user AGENTS.md instructions applied to this key only."""
role = role if role in ROLE_SCOPES else "user"
scopes = scopes or ROLE_SCOPES[role]
raw = "ant_" + secrets.token_urlsafe(32)
prefix = raw[:12] + "…"
if ttl_days is None:
ttl_days = config.DEFAULT_API_KEY_TTL_DAYS
expires_at = None if ttl_days == 0 else datetime.now(timezone.utc) + timedelta(days=ttl_days)
if max_sessions in (None, 0):
max_sessions = None # unlimited
agents_md = (agents_md or "").strip() or None
rec = db.create_api_key(_hash_key(raw), prefix, label, role, scopes, created_by,
expires_at, max_sessions, agents_md)
rec["api_key"] = raw # shown once
rec["expires_at"] = expires_at.isoformat() if expires_at else "lifetime"
rec["max_sessions"] = max_sessions or "unlimited"
return rec
def validate_api_key(raw: str) -> dict | None:
"""Return the key record if valid (exists, not revoked, not expired), else None."""
if not raw:
return None
rec = db.get_api_key_by_hash(_hash_key(raw))
if not rec or rec["revoked"]:
return None
if rec["expires_at"] and rec["expires_at"] <= datetime.now(timezone.utc):
return None
return rec
def scopes_for(rec: dict) -> list[str]:
return rec.get("scopes") or ROLE_SCOPES.get(rec.get("role", "user"), ROLE_SCOPES["user"])
|