Spaces:
Running
Running
File size: 2,517 Bytes
742e389 | 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 | """Tokens and roles."""
from __future__ import annotations
import base64
import hashlib
import hmac
import secrets
from dataclasses import dataclass
TOKEN_PREFIX = "fbt_"
IDENTITY_TYPES = ("user", "agent", "system") # a person, an AI agent, a service account
HUMAN_ROLES = ("admin", "member", "viewer")
AGENT_ROLES = ("admin", "reader", "member", "guest")
ADMIN_ONLY_AGENT_ROLES = ("admin", "reader") # only admins can create (or own) these
ROLES = ("admin", "member", "viewer", "guest", "reader")
@dataclass(frozen=True)
class Principal:
id: int
issuer: str
external_id: str
identity_type: str
role: str
display_name: str | None
owner_id: int | None
can_create_channels: bool
credential_id: str
@property
def is_admin(self) -> bool:
return self.role == "admin"
@property
def is_human(self) -> bool:
return self.identity_type == "user"
@property
def sees_everything(self) -> bool:
return self.role in ("admin", "reader")
@property
def may_post(self) -> bool:
return self.role not in ("viewer", "reader")
@property
def may_create_channels(self) -> bool:
if self.role == "admin":
return True
if self.is_human:
return self.role == "member"
return self.role == "member" and self.can_create_channels
@property
def may_invite_agents(self) -> bool:
return self.role == "admin" or (self.is_human and self.role == "member")
def new_token() -> tuple[str, str, bytes]:
"""Return (credential_id, full token, sha256 of secret)."""
cred_id = secrets.token_hex(8)
secret = secrets.token_bytes(32)
secret_text = base64.urlsafe_b64encode(secret).rstrip(b"=").decode()
return cred_id, f"{TOKEN_PREFIX}{cred_id}.{secret_text}", hashlib.sha256(secret).digest()
def parse_token(token: str) -> tuple[str, bytes] | None:
"""Split a token into (credential_id, secret bytes); None if malformed."""
if not token.startswith(TOKEN_PREFIX):
return None
try:
cred_id, secret_text = token[len(TOKEN_PREFIX):].split(".", 1)
secret = base64.urlsafe_b64decode(secret_text + "=" * (-len(secret_text) % 4))
except (ValueError, TypeError):
return None
if len(cred_id) != 16 or len(secret) != 32:
return None
return cred_id, secret
def secret_matches(secret: bytes, stored_hash: bytes) -> bool:
return hmac.compare_digest(hashlib.sha256(secret).digest(), bytes(stored_hash))
|