feat: updated saas platform
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""A key an integration authenticates with.
|
||||
|
||||
The model holds the hash and the prefix; the raw key exists once, in the response
|
||||
to creating it, and nowhere afterwards.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.config.database import Base
|
||||
|
||||
|
||||
def hash_secret(raw: str) -> str:
|
||||
"""One place, so the write and the lookup cannot disagree.
|
||||
|
||||
SHA-256 rather than bcrypt for the same reason invitations use it: this is
|
||||
checked on every API request, and the secret is 256 bits of randomness
|
||||
rather than something a person chose, so there is nothing to slow down.
|
||||
"""
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True,
|
||||
server_default=func.gen_random_uuid())
|
||||
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||
nullable=False)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False)
|
||||
name = Column(String(120), nullable=False)
|
||||
prefix = Column(String(16), nullable=False)
|
||||
key_hash = Column(String(64), nullable=False)
|
||||
scopes = Column(JSONB, nullable=False, default=list)
|
||||
last_used_at = Column(DateTime(timezone=True))
|
||||
expires_at = Column(DateTime(timezone=True))
|
||||
revoked_at = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
|
||||
owner = relationship("User", lazy="raise")
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return bool(self.expires_at and self.expires_at <= datetime.now(timezone.utc))
|
||||
|
||||
@property
|
||||
def is_live(self) -> bool:
|
||||
return not (self.revoked_at or self.is_expired)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
if self.revoked_at:
|
||||
return "revoked"
|
||||
if self.is_expired:
|
||||
return "expired"
|
||||
return "active"
|
||||
|
||||
def scope_list(self) -> list[str]:
|
||||
"""Empty means "whatever the owner can do" rather than "nothing".
|
||||
|
||||
The alternative — empty meaning no permissions — would make a key with a
|
||||
forgotten scope list silently useless, and the failure would look like a
|
||||
platform bug rather than a configuration one.
|
||||
"""
|
||||
return list(self.scopes or [])
|
||||
Reference in New Issue
Block a user