chore(backend): bootstrap Python 3.12+ project skeleton (B0.1)
- pyproject.toml with ruff + mypy strict + pytest + coverage >=70% - Makefile with Docker/Podman auto-detect - Multi-stage Dockerfile (python:3.12-slim-bookworm, non-root user) - docker-compose.yml for Postgres dev DB - alembic.ini wired to src/mimic/db/migrations - scripts/postgres-init/00-roles.sql seeds the audit writer role - .env.example documents every MIMIC_* var (no secrets committed)
This commit is contained in:
23
backend/.env.example
Normal file
23
backend/.env.example
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Mimic backend — example env. Copy to .env (gitignored) and adapt.
|
||||||
|
|
||||||
|
MIMIC_ENV=development
|
||||||
|
MIMIC_SECRET_KEY=replace-me-with-secrets.token_urlsafe-32
|
||||||
|
MIMIC_FERNET_KEY=
|
||||||
|
|
||||||
|
# Database
|
||||||
|
POSTGRES_DB=mimic
|
||||||
|
POSTGRES_USER=mimic_app
|
||||||
|
POSTGRES_PASSWORD=mimic_dev_password
|
||||||
|
MIMIC_DATABASE_URL=postgresql+psycopg://mimic_app:mimic_dev_password@localhost:5432/mimic
|
||||||
|
MIMIC_DATABASE_AUDIT_URL=postgresql+psycopg://mimic_audit_writer:CHANGE_ME@localhost:5432/mimic
|
||||||
|
|
||||||
|
# Session / cookies
|
||||||
|
MIMIC_SESSION_COOKIE_SECURE=false
|
||||||
|
MIMIC_SESSION_COOKIE_SAMESITE=Lax
|
||||||
|
|
||||||
|
# CORS (frontend dev)
|
||||||
|
MIMIC_CORS_ORIGINS=http://localhost:5173
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
MIMIC_LOG_LEVEL=DEBUG
|
||||||
|
MIMIC_LOG_JSON=false
|
||||||
60
backend/Dockerfile
Normal file
60
backend/Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# --- Stage 1: build --------------------------------------------------------
|
||||||
|
FROM python:3.12-slim-bookworm AS build
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
# WeasyPrint native deps + libpq + build tools.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libpangoft2-1.0-0 \
|
||||||
|
libcairo2 \
|
||||||
|
libgdk-pixbuf-2.0-0 \
|
||||||
|
libffi-dev \
|
||||||
|
shared-mime-info \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY pyproject.toml README.md ./
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip wheel build \
|
||||||
|
&& pip wheel --wheel-dir /wheels --no-deps .
|
||||||
|
|
||||||
|
RUN pip install --prefix=/install --no-warn-script-location .
|
||||||
|
|
||||||
|
# --- Stage 2: runtime ------------------------------------------------------
|
||||||
|
FROM python:3.12-slim-bookworm AS runtime
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
FLASK_APP=mimic.app:create_app \
|
||||||
|
MIMIC_ENV=production
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpq5 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libpangoft2-1.0-0 \
|
||||||
|
libcairo2 \
|
||||||
|
libgdk-pixbuf-2.0-0 \
|
||||||
|
shared-mime-info \
|
||||||
|
tini \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& groupadd --system --gid 1001 mimic \
|
||||||
|
&& useradd --system --uid 1001 --gid 1001 --home-dir /app --shell /usr/sbin/nologin mimic
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /install /usr/local
|
||||||
|
COPY --chown=mimic:mimic src ./src
|
||||||
|
|
||||||
|
USER mimic
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
CMD ["gunicorn", "--worker-class", "gevent", "--workers", "1", "--bind", "0.0.0.0:5000", "mimic.app:create_app()"]
|
||||||
80
backend/Makefile
Normal file
80
backend/Makefile
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# --- Mimic backend Makefile ------------------------------------------------
|
||||||
|
# Detects Docker / Podman automatically (NF-platform).
|
||||||
|
|
||||||
|
PY ?= python3.12
|
||||||
|
VENV ?= .venv
|
||||||
|
PIP := $(VENV)/bin/pip
|
||||||
|
PYTHON := $(VENV)/bin/python
|
||||||
|
PYTEST := $(VENV)/bin/pytest
|
||||||
|
RUFF := $(VENV)/bin/ruff
|
||||||
|
MYPY := $(VENV)/bin/mypy
|
||||||
|
ALEMBIC := $(VENV)/bin/alembic
|
||||||
|
FLASK := $(VENV)/bin/flask
|
||||||
|
|
||||||
|
# Container runtime auto-detect: Docker (preferred) or Podman rootless.
|
||||||
|
CONTAINER ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
|
||||||
|
COMPOSE ?= $(shell command -v docker-compose 2>/dev/null || echo "$(CONTAINER) compose")
|
||||||
|
|
||||||
|
export FLASK_APP=mimic.app:create_app
|
||||||
|
|
||||||
|
.PHONY: help install lint fmt typecheck test test-int test-cov run \
|
||||||
|
db-up db-down db-migrate db-revision db-seed db-dump db-restore \
|
||||||
|
build clean
|
||||||
|
|
||||||
|
help:
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
|
||||||
|
awk 'BEGIN {FS=":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
install: ## Create venv and install dev dependencies
|
||||||
|
$(PY) -m venv $(VENV)
|
||||||
|
$(PIP) install --upgrade pip wheel
|
||||||
|
$(PIP) install -e ".[dev]"
|
||||||
|
|
||||||
|
lint: ## Ruff lint
|
||||||
|
$(RUFF) check src tests
|
||||||
|
|
||||||
|
fmt: ## Ruff format
|
||||||
|
$(RUFF) format src tests
|
||||||
|
$(RUFF) check --fix src tests
|
||||||
|
|
||||||
|
typecheck: ## Mypy strict
|
||||||
|
$(MYPY) src
|
||||||
|
|
||||||
|
test: ## Unit tests (SQLite)
|
||||||
|
$(PYTEST) tests/unit -v
|
||||||
|
|
||||||
|
test-int: ## Integration tests (testcontainers Postgres)
|
||||||
|
$(PYTEST) tests/integration -v -m integration
|
||||||
|
|
||||||
|
test-cov: ## Unit + integration with coverage report
|
||||||
|
$(PYTEST) --cov=mimic --cov-report=term-missing --cov-report=html
|
||||||
|
|
||||||
|
run: ## Run Flask dev server
|
||||||
|
$(FLASK) run --host 0.0.0.0 --port 5000 --debug
|
||||||
|
|
||||||
|
db-up: ## Start Postgres dev container
|
||||||
|
$(COMPOSE) up -d postgres
|
||||||
|
|
||||||
|
db-down: ## Stop Postgres dev container
|
||||||
|
$(COMPOSE) down
|
||||||
|
|
||||||
|
db-migrate: ## Apply migrations
|
||||||
|
$(ALEMBIC) upgrade head
|
||||||
|
|
||||||
|
db-revision: ## Generate migration (msg=...)
|
||||||
|
$(ALEMBIC) revision --autogenerate -m "$(msg)"
|
||||||
|
|
||||||
|
db-seed: ## Seed local dev data (TBD sprint 1)
|
||||||
|
@echo "TBD sprint 1"
|
||||||
|
|
||||||
|
db-dump: ## Manual DB dump (NF-state, R-O1)
|
||||||
|
$(VENV)/bin/mimic-cli db dump --out backups/mimic-$$(date +%Y%m%d-%H%M%S).sql
|
||||||
|
|
||||||
|
db-restore: ## Restore from dump (file=...)
|
||||||
|
$(VENV)/bin/mimic-cli db restore --file $(file)
|
||||||
|
|
||||||
|
build: ## Build wheel
|
||||||
|
$(PYTHON) -m build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(VENV) .pytest_cache .mypy_cache .ruff_cache htmlcov dist build *.egg-info
|
||||||
56
backend/README.md
Normal file
56
backend/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Mimic — backend
|
||||||
|
|
||||||
|
Sprint 0 skeleton. Python 3.12+ / Flask / SQLAlchemy 2 / Alembic / Pydantic 2.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── src/mimic/
|
||||||
|
│ ├── app.py # Flask app factory + SocketIO init
|
||||||
|
│ ├── config.py # Pydantic Settings
|
||||||
|
│ ├── extensions.py # db, migrate, socketio, login_manager
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── models/ # SQLAlchemy 2 typed models
|
||||||
|
│ │ ├── repositories/ # data access per aggregate
|
||||||
|
│ │ └── migrations/ # Alembic
|
||||||
|
│ ├── schemas/ # Pydantic 2 DTOs
|
||||||
|
│ ├── api/ # Flask blueprints (REST)
|
||||||
|
│ ├── ws/ # Flask-SocketIO namespaces
|
||||||
|
│ ├── connectors/ # C2Connector ABC + payload mapping
|
||||||
|
│ ├── orchestrator/ # run state machine (stub in sprint 0)
|
||||||
|
│ ├── templating/ # Jinja2 sandbox + regex_extract
|
||||||
|
│ ├── audit/ # append-only writer + rotation
|
||||||
|
│ ├── reporting/ # WeasyPrint builder (stub in sprint 0)
|
||||||
|
│ ├── rbac/ # group-based permission matrix (F11)
|
||||||
|
│ ├── importers/ # ATR + C2 journal (stub in sprint 0)
|
||||||
|
│ └── cli/ # mimic-cli (click)
|
||||||
|
└── tests/
|
||||||
|
├── unit/ # SQLite, pure logic
|
||||||
|
└── integration/ # testcontainers Postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install # uv venv + pip install -e .[dev]
|
||||||
|
make db-up # docker compose up -d postgres
|
||||||
|
make db-migrate # alembic upgrade head
|
||||||
|
make run # flask run (debug)
|
||||||
|
make test # pytest unit
|
||||||
|
make test-int # pytest integration (testcontainers)
|
||||||
|
make lint # ruff + mypy strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## What sprint 0 ships
|
||||||
|
|
||||||
|
- Full §8 data model + Alembic initial migration (Postgres-specific constraints: audit_log write-only role, soc_session hash, c2_credential Fernet column).
|
||||||
|
- `C2Connector` ABC + dataclasses + `payload_type` enum + factory. **No real Mythic/Home implementation** (blocked on PR1/PR2).
|
||||||
|
- Jinja2 SandboxedEnvironment + `regex_extract` filter (re2).
|
||||||
|
- Local auth (bcrypt + Flask session) + group-based RBAC matching the F11 permission matrix.
|
||||||
|
- Flat CRUD on engagements / hosts / TTPs / scenarios.
|
||||||
|
- pytest baseline + testcontainers Postgres scaffolding.
|
||||||
|
|
||||||
|
## Out of sprint 0
|
||||||
|
|
||||||
|
Orchestrator, WebSocket cockpit, real connectors, report generation, audit rotation.
|
||||||
39
backend/alembic.ini
Normal file
39
backend/alembic.ini
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = src/mimic/db/migrations
|
||||||
|
prepend_sys_path = src
|
||||||
|
version_path_separator = os
|
||||||
|
sqlalchemy.url =
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
23
backend/docker-compose.yml
Normal file
23
backend/docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: mimic-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-mimic}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-mimic_app}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mimic_dev_password}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5432:5432"
|
||||||
|
volumes:
|
||||||
|
- mimic_pgdata:/var/lib/postgresql/data
|
||||||
|
- ./scripts/postgres-init:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mimic_app} -d ${POSTGRES_DB:-mimic}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mimic_pgdata:
|
||||||
|
name: mimic_pgdata
|
||||||
147
backend/pyproject.toml
Normal file
147
backend/pyproject.toml
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling>=1.24"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mimic"
|
||||||
|
version = "0.1.0a0"
|
||||||
|
description = "Mimic — internal BAS platform (sprint 0 skeleton)"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
license = { text = "Proprietary" }
|
||||||
|
authors = [{ name = "RT" }]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"flask>=3.0,<4.0",
|
||||||
|
"flask-socketio>=5.3,<6.0",
|
||||||
|
"flask-login>=0.6.3,<1.0",
|
||||||
|
"flask-migrate>=4.0,<5.0",
|
||||||
|
"sqlalchemy>=2.0,<3.0",
|
||||||
|
"alembic>=1.13,<2.0",
|
||||||
|
"psycopg[binary]>=3.1,<4.0",
|
||||||
|
"pydantic>=2.6,<3.0",
|
||||||
|
"pydantic-settings>=2.2,<3.0",
|
||||||
|
"python-json-logger>=2.0,<3.0",
|
||||||
|
"structlog>=24.1,<25.0",
|
||||||
|
"bcrypt>=4.1,<5.0",
|
||||||
|
"cryptography>=42.0,<43.0",
|
||||||
|
"jinja2>=3.1,<4.0",
|
||||||
|
"google-re2>=1.1,<2.0",
|
||||||
|
"click>=8.1,<9.0",
|
||||||
|
"gevent>=24.2,<25.0",
|
||||||
|
"gevent-websocket>=0.10,<1.0",
|
||||||
|
"httpx>=0.27,<1.0",
|
||||||
|
"weasyprint>=61.0,<62.0",
|
||||||
|
"authlib>=1.3,<2.0",
|
||||||
|
"pyyaml>=6.0,<7.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0,<9.0",
|
||||||
|
"pytest-cov>=5.0,<6.0",
|
||||||
|
"pytest-flask>=1.3,<2.0",
|
||||||
|
"pytest-mock>=3.12,<4.0",
|
||||||
|
"testcontainers[postgres]>=4.4,<5.0",
|
||||||
|
"ruff>=0.4,<1.0",
|
||||||
|
"mypy>=1.10,<2.0",
|
||||||
|
"types-pyyaml>=6.0,<7.0",
|
||||||
|
"freezegun>=1.5,<2.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mimic-cli = "mimic.cli:cli"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/mimic"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["src/mimic", "README.md", "pyproject.toml"]
|
||||||
|
|
||||||
|
# -- Ruff -------------------------------------------------------------------
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
src = ["src", "tests"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", "F", "W", # pycodestyle / pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # bugbear
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"N", # pep8-naming
|
||||||
|
"S", # flake8-bandit (security)
|
||||||
|
"C4", # comprehensions
|
||||||
|
"DTZ", # datetime tz
|
||||||
|
"PIE",
|
||||||
|
"PT", # pytest
|
||||||
|
"RET",
|
||||||
|
"SIM",
|
||||||
|
"TID",
|
||||||
|
"PL",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"PLR0913", # too many args (Flask handlers + DI)
|
||||||
|
"S101", # assert in tests
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/**" = ["S101", "S105", "S106", "PLR2004"]
|
||||||
|
"src/mimic/db/migrations/**" = ["E501", "N999"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["mimic"]
|
||||||
|
|
||||||
|
# -- Mypy -------------------------------------------------------------------
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.12"
|
||||||
|
strict = true
|
||||||
|
warn_unreachable = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
show_error_codes = true
|
||||||
|
plugins = ["pydantic.mypy"]
|
||||||
|
exclude = ["src/mimic/db/migrations/versions/"]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"weasyprint.*",
|
||||||
|
"google.re2.*",
|
||||||
|
"re2",
|
||||||
|
"flask_socketio.*",
|
||||||
|
"flask_migrate.*",
|
||||||
|
"gevent.*",
|
||||||
|
"testcontainers.*",
|
||||||
|
"authlib.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
# -- Pytest -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
addopts = "-ra --strict-markers --strict-config"
|
||||||
|
markers = [
|
||||||
|
"integration: requires testcontainers Postgres",
|
||||||
|
"slow: long-running tests",
|
||||||
|
]
|
||||||
|
filterwarnings = ["error"]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
source = ["src/mimic"]
|
||||||
|
omit = ["src/mimic/db/migrations/*"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
fail_under = 70
|
||||||
|
show_missing = true
|
||||||
|
skip_covered = false
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
"\\.\\.\\.",
|
||||||
|
]
|
||||||
20
backend/scripts/postgres-init/00-roles.sql
Normal file
20
backend/scripts/postgres-init/00-roles.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- Roles used by the application.
|
||||||
|
-- NF-AUDIT: audit_log must be append-only at the SQL level. The application
|
||||||
|
-- writes via mimic_audit_writer (INSERT only). The standard mimic_app role
|
||||||
|
-- has SELECT on audit_log but no UPDATE/DELETE.
|
||||||
|
--
|
||||||
|
-- This file runs once at container init. Production deployment uses Ansible
|
||||||
|
-- to apply the same grants idempotently.
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'mimic_audit_writer') THEN
|
||||||
|
CREATE ROLE mimic_audit_writer LOGIN PASSWORD 'CHANGE_ME';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- The mimic_app user is created by the official image entrypoint
|
||||||
|
-- via $POSTGRES_USER. We only need to make sure the audit writer exists.
|
||||||
|
-- Per-table grants are applied by the application's bootstrap step after
|
||||||
|
-- migrations land (so the audit_log table actually exists).
|
||||||
Reference in New Issue
Block a user