fix(release): keep owners and grants in production recovery points
Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
d13250f40a
commit
09906cdccc
@@ -5,12 +5,16 @@
|
||||
# Never restores into or migrates the database named jyotisha.
|
||||
#
|
||||
# Host mode runs every SQL statement inside the postgres container.
|
||||
# It does not call node on the host. Exit 4 means: after this rehearsal
|
||||
# copy was restored and migrated, service_role still cannot SELECT
|
||||
# public.chart_profiles. Grants come from the backup plus migrations,
|
||||
# not from a live probe of the database that is serving production.
|
||||
# If removing the rehearsal database fails, the script exits non-zero
|
||||
# and names that database so it can be dropped by hand.
|
||||
# It does not call node on the host. Restore keeps the owners and grants
|
||||
# stored in the backup. Migrations then run as schema_owner.
|
||||
# Exit 1: refused, including when a migration removed a table or column
|
||||
# grant that was present on the restored copy.
|
||||
# Exit 4: service_role still lacks column SELECT on public.chart_profiles
|
||||
# for the columns named by BUG-1063. Do not migrate production.
|
||||
# Exit 75: another production mutation holds the host lock.
|
||||
# Exit 0 prints rehearsal_ok=1 only after those checks, and only when the
|
||||
# rehearsal database was removed. A failed drop is non-zero and names
|
||||
# that database so it can be dropped by hand.
|
||||
set -euo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
@@ -98,11 +102,12 @@ print_safe_log() {
|
||||
}
|
||||
|
||||
run_node_migrator() {
|
||||
local batch="$1" log="$2" status
|
||||
local batch="$1" log="$2" status migrate_url
|
||||
migrate_url="${SCHEMA_DATABASE_URL}?options=-c%20role%3Dschema_owner"
|
||||
set +e
|
||||
(
|
||||
cd "$frontend_dir"
|
||||
MIGRATIONS_DIRECTORY="$batch" node scripts/db-migrate.mjs
|
||||
SCHEMA_DATABASE_URL="$migrate_url" MIGRATIONS_DIRECTORY="$batch" node scripts/db-migrate.mjs
|
||||
) >"$log" 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
@@ -131,33 +136,58 @@ if (kind === "count") {
|
||||
const count = result.rows[0]?.n ?? "";
|
||||
if (!/^[0-9]+$/.test(count)) process.exit(2);
|
||||
process.stdout.write(count);
|
||||
} else if (kind === "archived") {
|
||||
const result = await client.query(
|
||||
"select case when exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'chat_sessions' and column_name = 'archived_at') then (select count(*)::text from public.chat_sessions where archived_at is not null) else 'absent' end as n",
|
||||
} else if (kind === "archived" || kind === "self") {
|
||||
const spec = kind === "archived"
|
||||
? { table: "chat_sessions", column: "archived_at", whereSql: "archived_at is not null" }
|
||||
: { table: "chart_profiles", column: "role", whereSql: "role = 'self'" };
|
||||
const exists = await client.query(
|
||||
"select exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = $1 and column_name = $2) as present",
|
||||
[spec.table, spec.column],
|
||||
);
|
||||
const count = result.rows[0]?.n ?? "";
|
||||
if (!/^[0-9]+$/.test(count) && count !== "absent") process.exit(2);
|
||||
process.stdout.write(count);
|
||||
} else if (kind === "self") {
|
||||
const result = await client.query(
|
||||
"select case when exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'chart_profiles' and column_name = 'role') then (select count(*)::text from public.chart_profiles where role = 'self') else 'absent' end as n",
|
||||
);
|
||||
const count = result.rows[0]?.n ?? "";
|
||||
if (!/^[0-9]+$/.test(count) && count !== "absent") process.exit(2);
|
||||
process.stdout.write(count);
|
||||
} else if (kind === "smoke") {
|
||||
const checks = [
|
||||
["authenticated", "public.profiles"],
|
||||
["authenticated", "public.chat_sessions"],
|
||||
["service_role", "public.chart_profiles"],
|
||||
];
|
||||
for (const [role, table] of checks) {
|
||||
let count = "absent";
|
||||
if (exists.rows[0]?.present === true) {
|
||||
const result = await client.query(
|
||||
"select has_table_privilege($1, $2, 'SELECT') as ok",
|
||||
[role, table],
|
||||
`select count(*)::text as n from public.${spec.table} where ${spec.whereSql}`,
|
||||
);
|
||||
process.stdout.write(`${role} ${table} ${result.rows[0].ok}\n`);
|
||||
count = result.rows[0]?.n ?? "";
|
||||
}
|
||||
if (!/^[0-9]+$/.test(count) && count !== "absent") process.exit(2);
|
||||
process.stdout.write(count);
|
||||
} else if (kind === "grants") {
|
||||
const result = await client.query(`
|
||||
select line from (
|
||||
select grantee || '|' || privilege_type || '|' || table_schema || '|' || table_name as line
|
||||
from information_schema.role_table_grants
|
||||
where table_schema not in ('pg_catalog', 'information_schema')
|
||||
union all
|
||||
select grantee || '|' || privilege_type || '|' || table_schema || '|' || table_name || '|' || column_name
|
||||
from information_schema.column_privileges
|
||||
where table_schema not in ('pg_catalog', 'information_schema')
|
||||
) s
|
||||
order by 1
|
||||
`);
|
||||
for (const row of result.rows) process.stdout.write(`${row.line}\n`);
|
||||
} else if (kind === "bug1063") {
|
||||
const columns = [
|
||||
"id", "user_id", "role", "name", "birth_date", "reported_birth_time",
|
||||
"birth_time_source", "birth_time_period", "declared_window_start",
|
||||
"declared_window_end", "uncertainty_before_minutes", "uncertainty_after_minutes",
|
||||
"latitude", "longitude", "timezone_id", "timezone_offset", "birth_place_label",
|
||||
"birth_place_type", "birth_place_provider", "birth_place_provider_id", "ayanamsa",
|
||||
"active_birth_time", "active_birth_date", "active_birth_timezone_offset",
|
||||
"birth_time_status", "gender",
|
||||
];
|
||||
const result = await client.query(
|
||||
`select count(*)::text as n
|
||||
from unnest($1::text[]) as cols(column_name)
|
||||
where not has_column_privilege('service_role', 'public.chart_profiles'::regclass, cols.column_name, 'SELECT')`,
|
||||
[columns],
|
||||
);
|
||||
const missing = result.rows[0]?.n ?? "";
|
||||
if (!/^[0-9]+$/.test(missing)) process.exit(2);
|
||||
process.stdout.write(missing);
|
||||
} else if (kind === "grant_create") {
|
||||
await client.query(`grant connect, create on database ${name} to schema_owner`);
|
||||
} else if (kind === "drop") {
|
||||
await client.end();
|
||||
const admin = new URL(source);
|
||||
@@ -313,63 +343,9 @@ if [ "$local_mode" -eq 0 ]; then
|
||||
-pass env:PRODUCTION_BACKUP_ENCRYPTION_KEY -in "$RECOVERY_DUMP" |
|
||||
docker_cli exec -i -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
pg_restore -U "$POSTGRES_USER" -d "$database_name" \
|
||||
--no-owner --exit-on-error
|
||||
--exit-on-error
|
||||
set +o pipefail
|
||||
unset PRODUCTION_BACKUP_ENCRYPTION_KEY
|
||||
docker_cli exec -i -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d "$database_name" -v ON_ERROR_STOP=1 -f - <<'SQL'
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT n.nspname, c.relname, c.relkind
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
AND n.nspname NOT LIKE 'pg_%'
|
||||
AND c.relkind IN ('r', 'p', 'v', 'm', 'S', 'f')
|
||||
LOOP
|
||||
IF r.relkind = 'S' THEN
|
||||
EXECUTE format('ALTER SEQUENCE %I.%I OWNER TO schema_owner', r.nspname, r.relname);
|
||||
ELSIF r.relkind = 'v' THEN
|
||||
EXECUTE format('ALTER VIEW %I.%I OWNER TO schema_owner', r.nspname, r.relname);
|
||||
ELSIF r.relkind = 'm' THEN
|
||||
EXECUTE format('ALTER MATERIALIZED VIEW %I.%I OWNER TO schema_owner', r.nspname, r.relname);
|
||||
ELSE
|
||||
EXECUTE format('ALTER TABLE %I.%I OWNER TO schema_owner', r.nspname, r.relname);
|
||||
END IF;
|
||||
END LOOP;
|
||||
FOR r IN
|
||||
SELECT n.nspname, p.proname, pg_get_function_identity_arguments(p.oid) AS args
|
||||
FROM pg_proc p
|
||||
JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND n.nspname NOT LIKE 'pg_%'
|
||||
LOOP
|
||||
EXECUTE format('ALTER FUNCTION %I.%I(%s) OWNER TO schema_owner', r.nspname, r.proname, r.args);
|
||||
END LOOP;
|
||||
FOR r IN
|
||||
SELECT n.nspname, t.typname
|
||||
FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND n.nspname NOT LIKE 'pg_%'
|
||||
AND t.typtype IN ('e', 'c', 'd')
|
||||
LOOP
|
||||
EXECUTE format('ALTER TYPE %I.%I OWNER TO schema_owner', r.nspname, r.typname);
|
||||
END LOOP;
|
||||
FOR r IN
|
||||
SELECT n.nspname
|
||||
FROM pg_namespace n
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public')
|
||||
AND n.nspname NOT LIKE 'pg_%'
|
||||
LOOP
|
||||
EXECUTE format('ALTER SCHEMA %I OWNER TO schema_owner', r.nspname);
|
||||
END LOOP;
|
||||
EXECUTE 'ALTER SCHEMA public OWNER TO schema_owner';
|
||||
END
|
||||
$$;
|
||||
SQL
|
||||
# The URL is captured only into the environment, never echoed.
|
||||
SCHEMA_DATABASE_URL="$(python3 - "$POSTGRES_USER" "$POSTGRES_PASSWORD" "$database_name" <<'PY'
|
||||
import sys, urllib.parse
|
||||
@@ -407,6 +383,14 @@ else
|
||||
}
|
||||
fi
|
||||
|
||||
column_present() {
|
||||
local table="$1" column="$2" present
|
||||
present="$(docker_cli exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d "$database_name" -v ON_ERROR_STOP=1 -At -c \
|
||||
"select case when exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = '${table}' and column_name = '${column}') then 'yes' else 'no' end")"
|
||||
[ "$present" = "yes" ]
|
||||
}
|
||||
|
||||
sql_scalar() {
|
||||
local kind="$1" sql
|
||||
if [ "$local_mode" -eq 1 ]; then
|
||||
@@ -418,10 +402,18 @@ sql_scalar() {
|
||||
sql="select count(*)::text from public.chart_profiles where role = 'other' and (birth_time_status in ('accepted','confirmed') or active_birth_time is not null or active_birth_date is not null or active_birth_timezone_offset is not null)"
|
||||
;;
|
||||
archived)
|
||||
sql="select case when exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'chat_sessions' and column_name = 'archived_at') then (select count(*)::text from public.chat_sessions where archived_at is not null) else 'absent' end"
|
||||
if ! column_present chat_sessions archived_at; then
|
||||
printf 'absent'
|
||||
return
|
||||
fi
|
||||
sql="select count(*)::text from public.chat_sessions where archived_at is not null"
|
||||
;;
|
||||
self)
|
||||
sql="select case when exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'chart_profiles' and column_name = 'role') then (select count(*)::text from public.chart_profiles where role = 'self') else 'absent' end"
|
||||
if ! column_present chart_profiles role; then
|
||||
printf 'absent'
|
||||
return
|
||||
fi
|
||||
sql="select count(*)::text from public.chart_profiles where role = 'self'"
|
||||
;;
|
||||
*)
|
||||
return 2
|
||||
@@ -431,14 +423,34 @@ sql_scalar() {
|
||||
psql -U "$POSTGRES_USER" -d "$database_name" -v ON_ERROR_STOP=1 -At -c "$sql"
|
||||
}
|
||||
|
||||
sql_smoke() {
|
||||
capture_grants() {
|
||||
if [ "$local_mode" -eq 1 ]; then
|
||||
run_node_sql smoke
|
||||
run_node_sql grants
|
||||
return
|
||||
fi
|
||||
docker_cli exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d "$database_name" -v ON_ERROR_STOP=1 -At -c \
|
||||
"select line from (select 1 as n, 'authenticated public.profiles ' || case when has_table_privilege('authenticated', 'public.profiles', 'SELECT') then 'true' else 'false' end as line union all select 2, 'authenticated public.chat_sessions ' || case when has_table_privilege('authenticated', 'public.chat_sessions', 'SELECT') then 'true' else 'false' end union all select 3, 'service_role public.chart_profiles ' || case when has_table_privilege('service_role', 'public.chart_profiles', 'SELECT') then 'true' else 'false' end) s order by n"
|
||||
"select line from (select grantee || '|' || privilege_type || '|' || table_schema || '|' || table_name as line from information_schema.role_table_grants where table_schema not in ('pg_catalog', 'information_schema') union all select grantee || '|' || privilege_type || '|' || table_schema || '|' || table_name || '|' || column_name from information_schema.column_privileges where table_schema not in ('pg_catalog', 'information_schema')) s order by 1"
|
||||
}
|
||||
|
||||
chart_profiles_select_missing() {
|
||||
if [ "$local_mode" -eq 1 ]; then
|
||||
run_node_sql bug1063
|
||||
return
|
||||
fi
|
||||
docker_cli exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d "$database_name" -v ON_ERROR_STOP=1 -At -c \
|
||||
"select count(*)::text from (values ('id'),('user_id'),('role'),('name'),('birth_date'),('reported_birth_time'),('birth_time_source'),('birth_time_period'),('declared_window_start'),('declared_window_end'),('uncertainty_before_minutes'),('uncertainty_after_minutes'),('latitude'),('longitude'),('timezone_id'),('timezone_offset'),('birth_place_label'),('birth_place_type'),('birth_place_provider'),('birth_place_provider_id'),('ayanamsa'),('active_birth_time'),('active_birth_date'),('active_birth_timezone_offset'),('birth_time_status'),('gender')) as cols(column_name) where not has_column_privilege('service_role', 'public.chart_profiles'::regclass, cols.column_name, 'SELECT')"
|
||||
}
|
||||
|
||||
grant_migration_database() {
|
||||
if [ "$local_mode" -eq 1 ]; then
|
||||
run_node_sql grant_create
|
||||
return
|
||||
fi
|
||||
docker_cli exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d "$database_name" -v ON_ERROR_STOP=1 \
|
||||
-c "GRANT CONNECT, CREATE ON DATABASE \"${database_name}\" TO schema_owner;" >/dev/null
|
||||
}
|
||||
|
||||
echo "rehearsal_database=$database_name"
|
||||
@@ -448,6 +460,9 @@ archived_before="$(sql_scalar archived)" || refuse "could not count archived ses
|
||||
self_before="$(sql_scalar self)" || refuse "could not count self chart rows before migrations"
|
||||
echo "archived_sessions_before=$archived_before"
|
||||
echo "self_chart_rows_before=$self_before"
|
||||
grants_before="$work_dir/grants-before.txt"
|
||||
capture_grants >"$grants_before" || refuse "could not read grants before migrations"
|
||||
grant_migration_database || refuse "could not grant the migration role on the rehearsal database"
|
||||
set +e
|
||||
run_batch "$work_dir/before" "$work_dir/before.log"
|
||||
before_status=$?
|
||||
@@ -481,19 +496,20 @@ for log in "$work_dir/before.log" "$work_dir/all.log"; do
|
||||
notice_lines=$((notice_lines + $(grep -c '^notice ' "$log" || true)))
|
||||
done
|
||||
echo "notice_lines=$notice_lines"
|
||||
smoke_file="$work_dir/smoke.txt"
|
||||
if ! sql_smoke >"$smoke_file"; then
|
||||
refuse "rehearsal smoke could not check role privileges"
|
||||
grants_after="$work_dir/grants-after.txt"
|
||||
capture_grants >"$grants_after" || refuse "could not read grants after migrations"
|
||||
revoked="$(comm -23 <(sort "$grants_before") <(sort "$grants_after") | wc -l | tr -d '[:space:]')"
|
||||
echo "revoked_grants=$revoked"
|
||||
if [ "$revoked" != 0 ]; then
|
||||
refuse "migration revoked existing grants"
|
||||
fi
|
||||
cat "$smoke_file"
|
||||
if ! grep -q '^authenticated public.profiles true$' "$smoke_file"; then
|
||||
refuse "web role cannot read profiles"
|
||||
if ! missing="$(chart_profiles_select_missing | tr -d '[:space:]')"; then
|
||||
refuse "could not check chart_profiles column grants"
|
||||
fi
|
||||
if ! grep -q '^authenticated public.chat_sessions true$' "$smoke_file"; then
|
||||
refuse "web role cannot read chat sessions"
|
||||
fi
|
||||
if ! grep -q '^service_role public.chart_profiles true$' "$smoke_file"; then
|
||||
echo "stop: rehearsal copy still has no service_role SELECT on chart_profiles after migrations; do not migrate production" >&2
|
||||
[[ "$missing" =~ ^[0-9]+$ ]] || refuse "could not check chart_profiles column grants"
|
||||
echo "chart_profiles_service_role_select_missing=$missing"
|
||||
if [ "$missing" != 0 ]; then
|
||||
echo "stop: rehearsal copy still lacks service_role column SELECT on chart_profiles after migrations; do not migrate production" >&2
|
||||
exit 4
|
||||
fi
|
||||
echo "rehearsal_ok=1"
|
||||
|
||||
@@ -34,6 +34,18 @@ if [ "$ALLOW_ROLLBACK" != "true" ] && [ "$ALLOW_ROLLBACK" != "false" ]; then
|
||||
echo "invalid rollback authorization" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ALLOW_ROLLBACK" = "true" ]; then
|
||||
recovery_baseline="$DEPLOY_PATH/.state/recovery-baseline.env"
|
||||
[ -f "$recovery_baseline" ] && [ ! -L "$recovery_baseline" ] || {
|
||||
echo "recovery format is below 2; dispatch Create production recovery first" >&2
|
||||
exit 1
|
||||
}
|
||||
recovery_format="$(awk -F= '$1 == "RECOVERY_FORMAT" { print $2 }' "$recovery_baseline")"
|
||||
[[ "$recovery_format" =~ ^[0-9]+$ ]] && (( recovery_format >= 2 )) || {
|
||||
echo "recovery format is below 2; dispatch Create production recovery first" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
if [ "$VERIFICATION_MODE" != "internal" ] && [ "$VERIFICATION_MODE" != "public" ]; then
|
||||
echo "invalid production verification mode" >&2
|
||||
exit 1
|
||||
|
||||
@@ -51,6 +51,16 @@ esac
|
||||
echo "production recovery point must have restore_verified=true" >&2
|
||||
exit 1
|
||||
}
|
||||
recovery_baseline="$DEPLOY_PATH/.state/recovery-baseline.env"
|
||||
[ -f "$recovery_baseline" ] && [ ! -L "$recovery_baseline" ] || {
|
||||
echo "recovery format is below 2; dispatch Create production recovery first" >&2
|
||||
exit 1
|
||||
}
|
||||
recovery_format="$(awk -F= '$1 == "RECOVERY_FORMAT" { print $2 }' "$recovery_baseline")"
|
||||
[[ "$recovery_format" =~ ^[0-9]+$ ]] && (( recovery_format >= 2 )) || {
|
||||
echo "recovery format is below 2; dispatch Create production recovery first" >&2
|
||||
exit 1
|
||||
}
|
||||
recovery_created_epoch="$(date -u -d "$RECOVERY_CREATED_AT" +%s 2>/dev/null)" || {
|
||||
echo "invalid production recovery creation time" >&2
|
||||
exit 1
|
||||
|
||||
@@ -88,6 +88,48 @@ set -a
|
||||
set +a
|
||||
: "${POSTGRES_DB:?}" "${POSTGRES_USER:?}" "${POSTGRES_PASSWORD:?}" "${PRODUCTION_BACKUP_ENCRYPTION_KEY:?}"
|
||||
|
||||
privilege_sql() {
|
||||
case "$1" in
|
||||
table_grants)
|
||||
printf '%s' "select grantee || '|' || privilege_type || '|' || table_name from information_schema.role_table_grants where table_schema = 'public' order by 1"
|
||||
;;
|
||||
column_privileges)
|
||||
printf '%s' "select grantee || '|' || privilege_type || '|' || table_name || '|' || column_name from information_schema.column_privileges where table_schema = 'public' order by 1"
|
||||
;;
|
||||
table_owners)
|
||||
printf '%s' "select c.relname || '|' || pg_get_userbyid(c.relowner) from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind in ('r', 'p') order by 1"
|
||||
;;
|
||||
*)
|
||||
echo "unknown privilege check" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
privilege_snapshot() {
|
||||
local database="$1" kind="$2" destination="$3"
|
||||
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d "$database" -At -v ON_ERROR_STOP=1 \
|
||||
-c "$(privilege_sql "$kind")" | sort >"$destination"
|
||||
}
|
||||
|
||||
privileges_match() {
|
||||
local kind="$1" source_file restore_file status=0
|
||||
source_file="$(mktemp)"
|
||||
restore_file="$(mktemp)"
|
||||
if ! privilege_snapshot "$POSTGRES_DB" "$kind" "$source_file"; then
|
||||
rm -f -- "$source_file" "$restore_file"
|
||||
return 1
|
||||
fi
|
||||
if ! privilege_snapshot "$restore_database" "$kind" "$restore_file"; then
|
||||
rm -f -- "$source_file" "$restore_file"
|
||||
return 1
|
||||
fi
|
||||
cmp -s "$source_file" "$restore_file" || status=$?
|
||||
rm -f -- "$source_file" "$restore_file"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
usage_percent="$(df -Pk "$backup_directory" | awk 'NR == 2 {gsub(/%/, "", $5); print $5}')"
|
||||
[[ "$usage_percent" =~ ^[0-9]+$ ]] && (( usage_percent < 70 )) || {
|
||||
echo "production backup disk usage must remain below 70 percent" >&2
|
||||
@@ -111,7 +153,7 @@ cleanup() {
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom --no-owner --no-acl |
|
||||
pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom |
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 \
|
||||
-pass env:PRODUCTION_BACKUP_ENCRYPTION_KEY >"$backup_partial"
|
||||
[ -s "$backup_partial" ]
|
||||
@@ -127,7 +169,7 @@ openssl enc -d -aes-256-cbc -pbkdf2 \
|
||||
-pass env:PRODUCTION_BACKUP_ENCRYPTION_KEY -in "$backup_file" |
|
||||
sudo -n docker exec -i -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
pg_restore -U "$POSTGRES_USER" -d "$restore_database" \
|
||||
--no-owner --no-acl --exit-on-error
|
||||
--exit-on-error
|
||||
|
||||
counts="$({
|
||||
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
@@ -146,6 +188,17 @@ for value in "$identity_users" "$profiles" "$credit_transactions" "$public_table
|
||||
}
|
||||
done
|
||||
|
||||
table_grants_match=false
|
||||
column_privileges_match=false
|
||||
table_owners_match=false
|
||||
if privileges_match table_grants; then table_grants_match=true; fi
|
||||
if privileges_match column_privileges; then column_privileges_match=true; fi
|
||||
if privileges_match table_owners; then table_owners_match=true; fi
|
||||
if [ "$table_grants_match" != true ] || [ "$column_privileges_match" != true ] || [ "$table_owners_match" != true ]; then
|
||||
echo "restore privilege verification failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_container" \
|
||||
psql -U "$POSTGRES_USER" -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "DROP DATABASE \"$restore_database\" WITH (FORCE);" >/dev/null
|
||||
@@ -154,14 +207,15 @@ trap - EXIT HUP INT TERM
|
||||
|
||||
verify_basename="${backup_basename%.dump.enc}-restore-verify.json"
|
||||
verify_file="$backup_directory/$verify_basename"
|
||||
printf '{\n "mode": "restore_verify",\n "ok": true,\n "backup": "%s",\n "sha256": "%s",\n "created_at": "%s",\n "verified_at": "%s",\n "restore_database_removed": true,\n "checks": {"identity_users": %s, "profiles": %s, "credit_transactions": %s, "public_tables": %s}\n}\n' \
|
||||
printf '{\n "mode": "restore_verify",\n "ok": true,\n "backup": "%s",\n "sha256": "%s",\n "created_at": "%s",\n "verified_at": "%s",\n "restore_database_removed": true,\n "checks": {"identity_users": %s, "profiles": %s, "credit_transactions": %s, "public_tables": %s, "table_grants_match": %s, "column_privileges_match": %s, "table_owners_match": %s}\n}\n' \
|
||||
"$backup_basename" "$backup_sha256" "$created_at" "$verified_at" \
|
||||
"$identity_users" "$profiles" "$credit_transactions" "$public_tables" >"$verify_file"
|
||||
"$identity_users" "$profiles" "$credit_transactions" "$public_tables" \
|
||||
"$table_grants_match" "$column_privileges_match" "$table_owners_match" >"$verify_file"
|
||||
chmod 600 "$verify_file"
|
||||
|
||||
recovery_reference="gitea-actions-run-${RECOVERY_RUN_ID}/${backup_basename}"
|
||||
state_tmp="$state_directory/recovery-baseline.env.tmp.$$"
|
||||
printf 'RECOVERY_REFERENCE=%s\nRECOVERY_CREATED_AT=%s\nRECOVERY_SHA256=%s\nRESTORE_VERIFIED=true\nVERIFY_FILE=%s\n' \
|
||||
printf 'RECOVERY_REFERENCE=%s\nRECOVERY_CREATED_AT=%s\nRECOVERY_SHA256=%s\nRESTORE_VERIFIED=true\nRECOVERY_FORMAT=2\nVERIFY_FILE=%s\n' \
|
||||
"$recovery_reference" "$created_at" "$backup_sha256" "$verify_basename" >"$state_tmp"
|
||||
chmod 600 "$state_tmp"
|
||||
mv "$state_tmp" "$state_directory/recovery-baseline.env"
|
||||
|
||||
Reference in New Issue
Block a user