Milestone 3

This commit is contained in:
Knacky
2026-05-11 06:05:27 +02:00
commit 4c25e198fc
125 changed files with 13489 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
"""DB layer — base, session, mixins, shared enums."""
from app.db.base import Base
from app.db.mixins import SoftDeleteMixin, TimestampMixin, UuidPkMixin
from app.db.session import get_engine, get_sessionmaker, session_scope
__all__ = [
"Base",
"SoftDeleteMixin",
"TimestampMixin",
"UuidPkMixin",
"get_engine",
"get_sessionmaker",
"session_scope",
]

23
backend/app/db/base.py Normal file
View File

@@ -0,0 +1,23 @@
"""Declarative base for all ORM models.
Naming convention is set explicitly so Alembic generates stable, reviewable
constraint names across migrations and Postgres versions.
"""
from __future__ import annotations
from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase
# https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
NAMING_CONVENTION = {
"ix": "ix_%(table_name)s_%(column_0_N_name)s",
"uq": "uq_%(table_name)s_%(column_0_N_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_N_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=NAMING_CONVENTION)

56
backend/app/db/mixins.py Normal file
View File

@@ -0,0 +1,56 @@
"""Reusable column mixins.
Pattern: subclass `Base, TimestampMixin, SoftDeleteMixin` to get the columns.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column
class UuidPkMixin:
"""Native UUID primary key, generated Python-side."""
id: Mapped[uuid.UUID] = mapped_column(
Uuid(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
nullable=False,
)
class TimestampMixin:
"""`created_at` / `updated_at` server-managed timestamps (UTC)."""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
class SoftDeleteMixin:
"""Soft delete via a nullable `deleted_at` column.
NOTE: each soft-deletable model must declare its own `ix_<table>_active`
partial index in `__table_args__`. We deliberately don't auto-inject one
here because SQLAlchemy's `__table_args__` from a mixin gets clobbered as
soon as the model class declares its own — silently dropping the index.
Declaring it explicitly keeps the contract visible at the model site.
"""
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
default=None,
)

47
backend/app/db/session.py Normal file
View File

@@ -0,0 +1,47 @@
"""Engine + sessionmaker. Lazily initialised so test code can swap the URL."""
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, sessionmaker
from app.core.config import settings
_engine: Engine | None = None
_SessionLocal: sessionmaker[Session] | None = None
def get_engine() -> Engine:
global _engine
if _engine is None:
_engine = create_engine(
settings.database_url,
pool_pre_ping=True,
future=True,
)
return _engine
def get_sessionmaker() -> sessionmaker[Session]:
global _SessionLocal
if _SessionLocal is None:
_SessionLocal = sessionmaker(bind=get_engine(), expire_on_commit=False, future=True)
return _SessionLocal
@contextmanager
def session_scope() -> Iterator[Session]:
"""Context manager that commits on success, rolls back on error."""
s = get_sessionmaker()()
try:
yield s
s.commit()
except Exception:
s.rollback()
raise
finally:
s.close()

27
backend/app/db/types.py Normal file
View File

@@ -0,0 +1,27 @@
"""Shared enum-like string sets used across models.
Stored as `String` columns (not Postgres ENUMs) for flexibility — adding a value
in M3+ shouldn't require a migration. CHECK constraints validate the value set
at the DB level.
"""
from __future__ import annotations
# Roles a user is hinted with on a mission. Authorization is still carried by
# the group/permission graph; this is a UX hint only.
MISSION_ROLE_HINTS = ("red", "blue")
# Mission lifecycle.
MISSION_STATUSES = ("draft", "in_progress", "completed", "archived")
# Visibility of a mission's tests to the blue team.
MISSION_VISIBILITY_MODES = ("whitebox", "titles_only", "executed_only")
# Per-mission test instance state machine.
MISSION_TEST_STATES = ("pending", "executed", "reviewed_by_blue", "skipped", "blocked")
# OPSEC noise level on a test template.
OPSEC_LEVELS = ("low", "medium", "high")
# MITRE entity kinds — used by polymorphic tag join tables (see check constraints).
MITRE_KINDS = ("tactic", "technique", "subtechnique")