Refactor database layer and auditing

This commit is contained in:
2025-05-10 08:38:57 +02:00
parent d866553ac1
commit 9ccd2f1d4d
20 changed files with 718 additions and 469 deletions

View File

@ -7,128 +7,182 @@ This might require some changes to these schemas.
"""
import enum
import logging
import uuid
from datetime import datetime
import sqlalchemy as sa
from sqlmodel import JSON, Column, DateTime, Field, Relationship, SQLModel
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
LOG = logging.getLogger(__name__)
class Client(SQLModel, table=True):
"""Client model."""
class SubSystem(enum.StrEnum):
"""Available subsystems."""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(unique=True)
description: str | None = None
public_key: str
ADMIN = enum.auto()
SSHD = enum.auto()
BACKEND = enum.auto()
TEST = enum.auto()
created_at: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"server_default": sa.func.now()},
nullable=False,
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"
id: Mapped[uuid.UUID] = mapped_column(
sa.Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(sa.String, unique=True)
description: Mapped[str | None] = mapped_column(sa.String, nullable=True)
public_key: Mapped[str] = mapped_column(sa.Text)
created_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
)
updated_at: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"onupdate": sa.func.now(), "server_default": sa.func.now()},
updated_at: Mapped[datetime | None] = mapped_column(
sa.DateTime(timezone=True),
server_default=sa.func.now(),
onupdate=sa.func.now(),
)
secrets: list["ClientSecret"] = Relationship(
back_populates="client", passive_deletes="all"
secrets: Mapped[list["ClientSecret"]] = relationship(
back_populates="client", passive_deletes=True
)
policies: list["ClientAccessPolicy"] = Relationship(back_populates="client")
policies: Mapped[list["ClientAccessPolicy"]] = relationship(back_populates="client")
class ClientAccessPolicy(SQLModel, table=True):
class ClientAccessPolicy(Base):
"""Client access policies."""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
source: str
client_id: uuid.UUID | None = Field(foreign_key="client.id", ondelete="CASCADE")
client: Client | None = Relationship(back_populates="policies")
__tablename__: str = "client_access_policy"
created_at: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"server_default": sa.func.now()},
nullable=False,
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: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"onupdate": sa.func.now(), "server_default": sa.func.now()},
updated_at: Mapped[datetime | None] = mapped_column(
sa.DateTime(timezone=True),
server_default=sa.func.now(),
onupdate=sa.func.now(),
)
class ClientSecret(SQLModel, table=True):
class ClientSecret(Base):
"""A client secret."""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str
description: str | None = None
client_id: uuid.UUID | None = Field(foreign_key="client.id", ondelete="CASCADE")
client: Client | None = Relationship(back_populates="secrets")
secret: str
invalidated: bool = Field(default=False)
created_at: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"server_default": sa.func.now()},
nullable=False,
__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")
invalidated: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
)
updated_at: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"onupdate": sa.func.now(), "server_default": sa.func.now()},
updated_at: Mapped[datetime | None] = mapped_column(
sa.DateTime(timezone=True),
server_default=sa.func.now(),
onupdate=sa.func.now(),
)
class AuditLog(SQLModel, table=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.
"""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
subsystem: str
message: str
operation: str
client_id: uuid.UUID | None = None
client_name: str | None = None
origin: str | None = None
Field(default=None, sa_column=Column(JSON))
timestamp: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"server_default": sa.func.now()},
nullable=False,
__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)
class APIClient(SQLModel, table=True):
"""Stores API Keys."""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
token: str
read_write: bool
created_at: datetime | None = Field(
default=None,
sa_type=sa.DateTime(timezone=True),
sa_column_kwargs={"server_default": sa.func.now()},
nullable=False,
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:
"""Create database."""
LOG.info("Running init_db")
SQLModel.metadata.create_all(engine)
"""Initialize database."""
Base.metadata.create_all(engine)