mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 10:03:45 +03:00
add security test 03
This commit is contained in:
parent
9a403ed32f
commit
9eeecb48a3
3 changed files with 96 additions and 2 deletions
57
alembic/versions/0032_repair_missing_email_columns.py
Normal file
57
alembic/versions/0032_repair_missing_email_columns.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""repair missing email auth columns in legacy prod databases
|
||||
|
||||
Revision ID: 0032_email_cols_fix
|
||||
Revises: 0031_pii_retention_and_consent
|
||||
Create Date: 2026-03-02
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0032_email_cols_fix"
|
||||
down_revision = "0031_pii_retention_and_consent"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _has_column(inspector: sa.Inspector, table: str, column: str) -> bool:
|
||||
return any(str(col.get("name")) == column for col in inspector.get_columns(table))
|
||||
|
||||
|
||||
def _has_index(inspector: sa.Inspector, table: str, index_name: str) -> bool:
|
||||
return any(str(idx.get("name")) == index_name for idx in inspector.get_indexes(table))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if not _has_column(inspector, "clients", "email"):
|
||||
op.add_column("clients", sa.Column("email", sa.String(length=255), nullable=True))
|
||||
inspector = sa.inspect(bind)
|
||||
if not _has_index(inspector, "clients", "ix_clients_email"):
|
||||
op.create_index("ix_clients_email", "clients", ["email"], unique=False)
|
||||
|
||||
if not _has_column(inspector, "requests", "client_email"):
|
||||
op.add_column("requests", sa.Column("client_email", sa.String(length=255), nullable=True))
|
||||
inspector = sa.inspect(bind)
|
||||
if not _has_index(inspector, "requests", "ix_requests_client_email"):
|
||||
op.create_index("ix_requests_client_email", "requests", ["client_email"], unique=False)
|
||||
|
||||
if not _has_column(inspector, "otp_sessions", "channel"):
|
||||
op.add_column("otp_sessions", sa.Column("channel", sa.String(length=16), nullable=True, server_default="SMS"))
|
||||
if not _has_column(inspector, "otp_sessions", "email"):
|
||||
op.add_column("otp_sessions", sa.Column("email", sa.String(length=255), nullable=True))
|
||||
op.execute("UPDATE otp_sessions SET channel = 'SMS' WHERE channel IS NULL")
|
||||
op.alter_column("otp_sessions", "channel", nullable=False, server_default=None)
|
||||
inspector = sa.inspect(bind)
|
||||
if not _has_index(inspector, "otp_sessions", "ix_otp_sessions_email"):
|
||||
op.create_index("ix_otp_sessions_email", "otp_sessions", ["email"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Intentionally no-op for safety on heterogeneous legacy databases.
|
||||
pass
|
||||
|
|
@ -206,3 +206,4 @@ echo $? # 0=OK, >0=ALERT
|
|||
- `make security-smoke` — `PASS`, отчет: `reports/security/security-smoke-20260302-125811.md`.
|
||||
- `./scripts/ops/security_smoke.sh https://ruakb.online` — `PASS`, отчет: `reports/security/security-smoke-20260302-130511.md` (TLS и security headers внешнего контура).
|
||||
- `./scripts/ops/security_smoke.sh https://ruakb.ru` — `PASS`, отчет: `reports/security/security-smoke-20260302-130536.md` (TLS и security headers внешнего контура).
|
||||
- `docker compose run --rm backend alembic upgrade head` — успешно после hotfix-миграции `0032_email_cols_fix` (ремонт legacy-схемы: `requests.client_email`, `otp_sessions.channel/email`, индексы).
|
||||
|
|
|
|||
|
|
@ -78,8 +78,20 @@ ensure_compose_files() {
|
|||
}
|
||||
|
||||
stack_up_and_migrate() {
|
||||
log "Starting production stack (nginx profile)"
|
||||
"${PROD_COMPOSE[@]}" up -d --build --remove-orphans db redis minio backend chat-service email-service worker beat frontend edge clamav
|
||||
log "Starting core infra services"
|
||||
"${PROD_COMPOSE[@]}" up -d --build --remove-orphans --force-recreate db redis minio clamav
|
||||
|
||||
log "Starting app services (backend/chat/email/worker/beat)"
|
||||
"${PROD_COMPOSE[@]}" up -d --build --remove-orphans --force-recreate backend chat-service email-service worker beat
|
||||
|
||||
log "Waiting app services to become healthy"
|
||||
wait_service_healthy "backend" 60
|
||||
wait_service_healthy "chat-service" 60
|
||||
wait_service_healthy "email-service" 60
|
||||
|
||||
# Force recreate frontend/edge after chat/backend to avoid stale DNS upstream cache in nginx.
|
||||
log "Starting/recreating frontend and edge"
|
||||
"${PROD_COMPOSE[@]}" up -d --build --remove-orphans --force-recreate frontend edge
|
||||
|
||||
log "Applying migrations"
|
||||
"${PROD_COMPOSE[@]}" exec -T backend alembic upgrade head
|
||||
|
|
@ -94,6 +106,30 @@ print("production security config validation: ok")
|
|||
PY
|
||||
}
|
||||
|
||||
wait_service_healthy() {
|
||||
local service="$1"
|
||||
local max_attempts="${2:-60}"
|
||||
local attempt=1
|
||||
local cid=""
|
||||
local status=""
|
||||
|
||||
while (( attempt <= max_attempts )); do
|
||||
cid="$("${PROD_COMPOSE[@]}" ps -q "$service" 2>/dev/null || true)"
|
||||
if [[ -n "$cid" ]]; then
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$cid" 2>/dev/null || true)"
|
||||
case "$status" in
|
||||
healthy|running)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
fail "Service '${service}' did not become healthy in time (last_status=${status:-unknown})"
|
||||
}
|
||||
|
||||
run_local_smoke() {
|
||||
if [[ "$SKIP_LOCAL_SMOKE" == "1" ]]; then
|
||||
log "Skipping local smoke checks (SKIP_LOCAL_SMOKE=1)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue