diff --git a/database/bootstrap/002_gen2_roles_and_schemas.sql b/database/bootstrap/002_gen2_roles_and_schemas.sql new file mode 100644 index 0000000..feb52fc --- /dev/null +++ b/database/bootstrap/002_gen2_roles_and_schemas.sql @@ -0,0 +1,58 @@ +-- Gen2 owner-role provisioning for the dedicated bootstrap/migration principal. +-- This is intentionally separate from feature migrations and may be run repeatedly. +BEGIN; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'world_role') THEN + CREATE ROLE world_role LOGIN; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'configuration_role') THEN + CREATE ROLE configuration_role LOGIN; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'self_role') THEN + CREATE ROLE self_role LOGIN; + END IF; +END +$$; + +DO $$ +DECLARE + role_name TEXT; + can_login BOOLEAN; +BEGIN + FOREACH role_name IN ARRAY ARRAY['world_role', 'configuration_role', 'self_role'] LOOP + SELECT rolcanlogin INTO can_login FROM pg_roles WHERE rolname = role_name; + IF can_login IS DISTINCT FROM TRUE THEN + RAISE EXCEPTION 'Gen2 owner role % exists but is not a V1-compatible LOGIN role', role_name; + END IF; + END LOOP; +END +$$; + +CREATE SCHEMA IF NOT EXISTS world AUTHORIZATION world_role; +CREATE SCHEMA IF NOT EXISTS configuration AUTHORIZATION configuration_role; +CREATE SCHEMA IF NOT EXISTS self AUTHORIZATION self_role; + +REVOKE ALL ON SCHEMA world, configuration, self FROM PUBLIC; + +DO $$ +DECLARE + schema_name TEXT; + expected_owner TEXT; + actual_owner TEXT; +BEGIN + FOREACH schema_name IN ARRAY ARRAY['world', 'configuration', 'self'] LOOP + expected_owner := schema_name || '_role'; + SELECT pg_get_userbyid(nspowner) + INTO actual_owner + FROM pg_namespace + WHERE nspname = schema_name; + IF actual_owner IS DISTINCT FROM expected_owner THEN + RAISE EXCEPTION 'Gen2 schema % is owned by %, expected %', schema_name, actual_owner, expected_owner; + END IF; + END LOOP; +END +$$; + +COMMIT; diff --git a/deploy/apply-migrations.sh b/deploy/apply-migrations.sh index 1f20d73..1bdf0d3 100755 --- a/deploy/apply-migrations.sh +++ b/deploy/apply-migrations.sh @@ -25,6 +25,16 @@ psql_exec() { docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@" } +# Owner roles and schemas are provisioned by the dedicated bootstrap boundary, +# before feature migrations run. This keeps role creation out of migrations and +# makes existing PostgreSQL volumes converge idempotently as well. +GEN2_BOOTSTRAP="$REPO_DIR/database/bootstrap/002_gen2_roles_and_schemas.sql" +if [[ ! -f "$GEN2_BOOTSTRAP" ]]; then + echo "migration runner: Gen2 bootstrap is missing" >&2 + exit 1 +fi +psql_exec < "$GEN2_BOOTSTRAP" + psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));" if [[ -n "${MIGRATION_BASELINE:-}" ]]; then diff --git a/deploy/images/postgres.Dockerfile b/deploy/images/postgres.Dockerfile index 648f34d..4a54ce1 100644 --- a/deploy/images/postgres.Dockerfile +++ b/deploy/images/postgres.Dockerfile @@ -4,5 +4,6 @@ LABEL org.opencontainers.image.revision=$SOURCE_REVISION \ org.opencontainers.image.source="https://git.cin.su/m/Assistant.git" COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql +COPY database/bootstrap/002_gen2_roles_and_schemas.sql /docker-entrypoint-initdb.d/002_gen2_roles_and_schemas.sql COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh RUN chmod 0555 /docker-entrypoint-initdb.d/002_owner_passwords.sh diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index b044ed2..d47846e 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1 +1 @@ -{"task_file":"dev/tasks/GEN2-027-production-canary-and-controlled-gen2-traffic-transition.json","task_id":"GEN2-027","task_revision":1} +{"task_file":"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json","task_id":"GEN2-DB-001","task_revision":1} diff --git a/dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json b/dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json new file mode 100644 index 0000000..cb8d370 --- /dev/null +++ b/dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json @@ -0,0 +1,42 @@ +{ + "task_id": "GEN2-DB-001", + "task_revision": 1, + "architecture_revision": "ARCH-GEN2-000", + "base_repo_revision": "gen2-integration", + "risk_tier": "E", + "intent": "Исправить provisioning Gen2 owner roles/schemas до применения migrations 046-050 без ослабления V1 DB isolation.", + "requirement_ids": ["REQ-GEN2-DB-001-ROLES", "REQ-GEN2-DB-001-OWNERSHIP", "REQ-GEN2-DB-001-IDEMPOTENCY", "REQ-GEN2-DB-001-RECOVERY"], + "allowed_change_scope": { + "modules": ["database bootstrap", "deployment migration runner"], + "paths": [ + "database/bootstrap/002_gen2_roles_and_schemas.sql", + "deploy/apply-migrations.sh", + "deploy/images/postgres.Dockerfile", + "dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json", + "dev/ACTIVE_TASK.json", + "dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json" + ], + }, + "forbidden_change_scope": { + "paths": [ + "GEN2-028", + "production-canary-before-provisioning", + "feature-migration-role-creation", + "cross-owner-grants", + "new canonical owner", + "production canary before provisioning" + ] + }, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["V1 database ownership/ACL", "GEN2 canonical owner registry", "GEN2 execution contract"], "may_change": ["Bootstrap provisioning only; feature migration semantics are unchanged."]}, + "semantic_owner": "Dedicated PostgreSQL bootstrap/migration principal; semantic ownership remains World, Configuration, SELF and existing V1 owners.", + "canonical_state": ["world schema owned by world_role", "configuration schema owned by configuration_role", "self schema owned by self_role", "no cross-owner canonical writes"], + "state_transitions": ["missing Gen2 roles/schemas -> idempotently provisioned -> migrations 046-050 apply -> restart/recovery preserves schema and migration state"], + "side_effects_and_privileges": ["Create only the three declared LOGIN owner roles when absent.", "Revoke PUBLIC schema privileges and fail closed on an unexpected schema owner.", "Do not invent or print credentials; do not grant cross-owner access."], + "persistence_and_migrations": ["Provisioning runs in the dedicated bootstrap boundary before feature migrations.", "Migrations 046-050 remain unchanged and are applied by the existing migration history runner."], + "failure_retry_restart": ["Provisioning is safe to run repeatedly.", "Unexpected existing role/schema ownership fails closed and requires an architecture decision.", "PostgreSQL restart/recovery must retain owner roles, schemas, ACL and migration history."], + "assumptions": ["This is a user-authorized remediation task for the previously stopped GEN2-027 migration decision.", "No Gen2 runtime consumer currently requires a new database password; credentials are not invented by this task.", "GEN2-027 resumes only after provisioning and recovery evidence; GEN2-028 is not started."], + "acceptance_scenarios": ["GEN2-DB-001-SCN-01: all roles required by migrations 046-050 are identified", "GEN2-DB-001-SCN-02: provisioning succeeds twice without duplicate objects", "GEN2-DB-001-SCN-03: migrations 046-050 apply in the controlled deployment path", "GEN2-DB-001-SCN-04: schema owners and PUBLIC ACL are confirmed", "GEN2-DB-001-SCN-05: PostgreSQL restart/recovery preserves the result", "GEN2-DB-001-EXIT: migration blocker is removed without weakening V1 isolation"], + "required_tests": ["FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST whitespace: git diff --check gen2-integration --", "TARGETED: provisioning repeated twice, migrations 046-050, ACL/ownership verification and PostgreSQL restart/recovery"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"] +}