"""Database models. TODO: We might want to pass on audit information from the SSH server. This might require some changes to these schemas. """ import enum import logging import uuid from datetime import datetime import sqlalchemy as sa from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship LOG = logging.getLogger(__name__) class SubSystem(enum.StrEnum): """Available subsystems.""" ADMIN = enum.auto() SSHD = enum.auto() BACKEND = enum.auto() TEST = enum.auto() class Operation(enum.StrEnum): """Various operations for the audit logging module.""" CREATE = enum.auto() READ = enum.auto() UPDATE = enum.auto() DELETE = enum.auto() DENY = enum.auto() PERMIT = enum.auto() LOGIN = enum.auto() REGISTER = enum.auto() NONE = enum.auto() class Base(DeclarativeBase): pass class Client(Base): """Clients.""" __tablename__: str = "client" __table_args__: tuple[sa.UniqueConstraint, ...] = ( sa.UniqueConstraint("name", "version", name="uq_client_name_version"), ) id: Mapped[uuid.UUID] = mapped_column( sa.Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4 ) version: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=1) name: Mapped[str] = mapped_column(sa.String, nullable=False) description: Mapped[str | None] = mapped_column(sa.String, nullable=True) public_key: Mapped[str] = mapped_column(sa.Text, nullable=False) is_active: Mapped[bool] = mapped_column(sa.Boolean, default=True) is_deleted: Mapped[bool] = mapped_column(sa.Boolean, default=False) is_system: Mapped[bool] = mapped_column(sa.Boolean, default=False) created_at: Mapped[datetime] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ) updated_at: Mapped[datetime | None] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), ) deleted_at: Mapped[datetime | None] = mapped_column( sa.DateTime(timezone=True), nullable=True ) secrets: Mapped[list["ClientSecret"]] = relationship( back_populates="client", passive_deletes=True ) previous_version_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), sa.ForeignKey("client.id", ondelete="SET NULL"), nullable=True, ) previous_version: Mapped["Client | None"] = relationship( "Client", remote_side=[id], backref="versions" ) policies: Mapped[list["ClientAccessPolicy"]] = relationship(back_populates="client") class ClientAccessPolicy(Base): """Client access policies.""" __tablename__: str = "client_access_policy" id: Mapped[uuid.UUID] = mapped_column( sa.Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4 ) source: Mapped[str] = mapped_column(sa.String) client_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), sa.ForeignKey("client.id", ondelete="CASCADE") ) client: Mapped[Client] = relationship(back_populates="policies") created_at: Mapped[datetime] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ) updated_at: Mapped[datetime | None] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), ) class ClientSecret(Base): """A client secret.""" __tablename__: str = "client_secret" id: Mapped[uuid.UUID] = mapped_column( sa.Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(sa.String) description: Mapped[str | None] = mapped_column(sa.String, nullable=True) secret: Mapped[str] = mapped_column(sa.String) client_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), sa.ForeignKey("client.id", ondelete="CASCADE") ) client: Mapped[Client] = relationship(back_populates="secrets") deleted: Mapped[bool] = mapped_column(default=False) is_system: Mapped[bool] = mapped_column(sa.Boolean, default=False) created_at: Mapped[datetime] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ) updated_at: Mapped[datetime | None] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), ) deleted_at: Mapped[datetime | None] = mapped_column( sa.DateTime(timezone=True), nullable=True ) class APIClient(Base): """A client on the API. This should eventually get more granular permissions. """ __tablename__: str = "api_client" id: Mapped[uuid.UUID] = mapped_column( sa.Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4 ) subsystem: Mapped[SubSystem | None] = mapped_column(sa.String, nullable=True) token: Mapped[str] = mapped_column(sa.String) created_at: Mapped[datetime] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ) updated_at: Mapped[datetime | None] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), ) class AuditLog(Base): """Audit log. This is implemented without any foreign keys to avoid losing data on deletions. """ __tablename__: str = "audit_log" id: Mapped[uuid.UUID] = mapped_column( sa.Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4 ) subsystem: Mapped[SubSystem] = mapped_column(sa.String) message: Mapped[str] = mapped_column(sa.String) operation: Mapped[Operation] = mapped_column(sa.String) client_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), nullable=True ) data: Mapped[dict[str, str] | None] = mapped_column(sa.JSON, nullable=True) client_name: Mapped[str | None] = mapped_column(sa.String, nullable=True) secret_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), nullable=True ) secret_name: Mapped[str | None] = mapped_column(sa.String, nullable=True) origin: Mapped[str | None] = mapped_column(sa.String, nullable=True) timestamp: Mapped[datetime] = mapped_column( sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ) def init_db(engine: sa.Engine) -> None: """Initialize database.""" Base.metadata.create_all(engine) async def init_db_async(engine: AsyncEngine) -> None: """Initialize database.""" async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)