ops: add guarded staging account reset
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
name: Reset Staging Account
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expected_deploy_sha:
|
||||
description: Exact 40-character SHA currently deployed to staging
|
||||
required: true
|
||||
type: string
|
||||
email:
|
||||
description: Exact staging account email
|
||||
required: true
|
||||
type: string
|
||||
confirmation:
|
||||
description: Type RESET followed by a space and the exact email
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: staging-mutation
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
reset:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.STAGING_URL }}
|
||||
env:
|
||||
DEPLOY_HOST: ${{ vars.STAGING_HOST }}
|
||||
DEPLOY_PORT: ${{ vars.STAGING_PORT }}
|
||||
DEPLOY_USER: ${{ vars.STAGING_USER }}
|
||||
DEPLOY_PATH: ${{ vars.STAGING_PATH }}
|
||||
STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }}
|
||||
EXPECTED_DEPLOY_SHA: ${{ inputs.expected_deploy_sha }}
|
||||
RESET_EMAIL: ${{ inputs.email }}
|
||||
RESET_CONFIRMATION: ${{ inputs.confirmation }}
|
||||
|
||||
steps:
|
||||
- name: Checkout trusted controller
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate account reset request and staging target
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]]
|
||||
test "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL"
|
||||
test "$DEPLOY_HOST" = "118.26.111.127"
|
||||
test "$DEPLOY_PORT" = "22"
|
||||
test "$DEPLOY_USER" = "deploy"
|
||||
test "$DEPLOY_PATH" = "/opt/jyotisha-staging"
|
||||
test -n "$STAGING_KNOWN_HOSTS"
|
||||
bash -n deploy/reset-staging-account.sh
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY"
|
||||
install -d -m 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Reset one staging account under host lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=10"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"DEPLOY_PATH='$DEPLOY_PATH' EXPECTED_DEPLOY_SHA='$EXPECTED_DEPLOY_SHA' RESET_EMAIL='$RESET_EMAIL' RESET_CONFIRMATION='$RESET_CONFIRMATION' bash -s" \
|
||||
< deploy/reset-staging-account.sh
|
||||
Executable
+290
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set +x
|
||||
|
||||
required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA RESET_EMAIL RESET_CONFIRMATION)
|
||||
for key in "${required[@]}"; do
|
||||
if [ -z "${!key:-}" ]; then
|
||||
echo "required staging account-reset input is missing: $key" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
[[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo "invalid expected deployment SHA" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]] || {
|
||||
echo "invalid reset email" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL" ] || {
|
||||
echo "account reset confirmation does not match" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$DEPLOY_PATH" = "/opt/jyotisha-staging" ] || {
|
||||
echo "refusing non-staging deployment path" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
state_directory="$DEPLOY_PATH/.state"
|
||||
[ -f "$state_directory/deployed-revision" ] || {
|
||||
echo "staging deployed revision is unavailable" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$(<"$state_directory/deployed-revision")" = "$EXPECTED_DEPLOY_SHA" ] || {
|
||||
echo "deployed staging revision does not match the approved reset SHA" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
install -d -m 700 "$state_directory"
|
||||
exec 9>"$state_directory/mutation.lock"
|
||||
flock -n 9 || {
|
||||
echo "another staging mutation holds the host lock" >&2
|
||||
exit 75
|
||||
}
|
||||
|
||||
cd "$DEPLOY_PATH"
|
||||
compose=(docker compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml)
|
||||
"${compose[@]}" ps --status running postgres --quiet | grep -q . || {
|
||||
echo "staging postgres container is not running" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_psql() {
|
||||
"${compose[@]}" exec -T -e RESET_EMAIL="$RESET_EMAIL" postgres sh -ceu '
|
||||
exec psql -X -v ON_ERROR_STOP=1 -v target_email="$RESET_EMAIL" \
|
||||
-U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
||||
'
|
||||
}
|
||||
|
||||
run_psql <<'SQL'
|
||||
begin;
|
||||
|
||||
create temporary table reset_snapshot on commit drop as
|
||||
select
|
||||
identity_user.id,
|
||||
identity_user.email,
|
||||
profile.email as profile_email,
|
||||
profile.credits,
|
||||
(select count(*) from identity.accounts value where value.user_id = identity_user.id) as identity_accounts,
|
||||
(select count(*) from identity.sessions value where value.user_id = identity_user.id) as identity_sessions,
|
||||
(select count(*) from public.credit_transactions value where value.user_id = identity_user.id) as credit_transactions,
|
||||
(select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id) as credit_cancellations,
|
||||
(select count(*) from public.consultation_requests value where value.user_id = identity_user.id) as consultation_requests,
|
||||
(select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id) as rectification_billing,
|
||||
(select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id) as action_receipts,
|
||||
(select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id) as redeemed_codes,
|
||||
(select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id) as admin_audit_logs
|
||||
from identity.users identity_user
|
||||
join auth.users auth_user on auth_user.id = identity_user.id
|
||||
join public.profiles profile on profile.id = identity_user.id
|
||||
where lower(btrim(identity_user.email)) = lower(btrim(:'target_email'))
|
||||
and lower(btrim(auth_user.email)) = lower(btrim(:'target_email'))
|
||||
for update of identity_user, auth_user, profile;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if (select count(*) from reset_snapshot) <> 1 then
|
||||
raise exception 'account_not_found_or_identity_bridge_mismatch';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
select jsonb_build_object(
|
||||
'stage', 'preflight',
|
||||
'email', snapshot.email,
|
||||
'credits', snapshot.credits,
|
||||
'identityAccounts', snapshot.identity_accounts,
|
||||
'identitySessions', snapshot.identity_sessions,
|
||||
'creditTransactions', snapshot.credit_transactions,
|
||||
'creditCancellations', snapshot.credit_cancellations,
|
||||
'consultationRequests', snapshot.consultation_requests,
|
||||
'rectificationBilling', snapshot.rectification_billing,
|
||||
'actionReceipts', snapshot.action_receipts,
|
||||
'redeemedCodes', snapshot.redeemed_codes,
|
||||
'adminAuditLogs', snapshot.admin_audit_logs,
|
||||
'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = snapshot.id),
|
||||
'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = snapshot.id),
|
||||
'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = snapshot.id),
|
||||
'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = snapshot.id),
|
||||
'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = snapshot.id),
|
||||
'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = snapshot.id),
|
||||
'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = snapshot.id),
|
||||
'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = snapshot.id)
|
||||
)
|
||||
from reset_snapshot snapshot;
|
||||
|
||||
update public.profiles profile
|
||||
set name = null,
|
||||
birth_date = null,
|
||||
birth_time = null,
|
||||
country_code = null,
|
||||
province_code = null,
|
||||
city_code = null,
|
||||
district_code = null,
|
||||
onboarding_payload = null,
|
||||
onboarding_version = null,
|
||||
onboarding_generated_at = null,
|
||||
latitude = null,
|
||||
longitude = null,
|
||||
timezone_offset = null,
|
||||
reported_birth_time = null,
|
||||
active_birth_time = null,
|
||||
birth_time_source = null,
|
||||
birth_time_period = null,
|
||||
birth_time_clue = null,
|
||||
uncertainty_before_minutes = null,
|
||||
uncertainty_after_minutes = null,
|
||||
birth_time_status = null,
|
||||
rectification_confidence = null,
|
||||
rectification_case_id = null,
|
||||
birth_place_label = null,
|
||||
birth_place_type = null,
|
||||
birth_place_provider = null,
|
||||
birth_place_provider_id = null,
|
||||
timezone_id = null,
|
||||
timezone_source = null,
|
||||
updated_at = pg_catalog.now()
|
||||
from reset_snapshot snapshot
|
||||
where profile.id = snapshot.id;
|
||||
|
||||
delete from public.chat_sessions value using reset_snapshot snapshot where value.user_id = snapshot.id;
|
||||
delete from public.chart_profiles value using reset_snapshot snapshot where value.user_id = snapshot.id;
|
||||
delete from public.synastry_reports value using reset_snapshot snapshot where value.user_id = snapshot.id;
|
||||
delete from public.birth_time_rectification_v4_cases value using reset_snapshot snapshot where value.user_id = snapshot.id;
|
||||
delete from public.birth_time_rectification_cases value using reset_snapshot snapshot where value.user_id = snapshot.id;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1
|
||||
from reset_snapshot snapshot
|
||||
join identity.users identity_user on identity_user.id = snapshot.id
|
||||
join auth.users auth_user on auth_user.id = snapshot.id
|
||||
join public.profiles profile on profile.id = snapshot.id
|
||||
where identity_user.email is distinct from snapshot.email
|
||||
or auth_user.email is distinct from snapshot.email
|
||||
or profile.email is distinct from snapshot.profile_email
|
||||
or profile.credits is distinct from snapshot.credits
|
||||
or (select count(*) from identity.accounts value where value.user_id = snapshot.id) <> snapshot.identity_accounts
|
||||
or (select count(*) from identity.sessions value where value.user_id = snapshot.id) <> snapshot.identity_sessions
|
||||
or (select count(*) from public.credit_transactions value where value.user_id = snapshot.id) <> snapshot.credit_transactions
|
||||
or (select count(*) from public.credit_request_cancellations value where value.user_id = snapshot.id) <> snapshot.credit_cancellations
|
||||
or (select count(*) from public.consultation_requests value where value.user_id = snapshot.id) <> snapshot.consultation_requests
|
||||
or (select count(*) from public.birth_time_rectification_billing value where value.user_id = snapshot.id) <> snapshot.rectification_billing
|
||||
or (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = snapshot.id) <> snapshot.action_receipts
|
||||
or (select count(*) from public.redemption_codes value where value.redeemed_by = snapshot.id) <> snapshot.redeemed_codes
|
||||
or (select count(*) from audit.admin_audit_logs value where value.actor_user_id = snapshot.id) <> snapshot.admin_audit_logs
|
||||
) then
|
||||
raise exception 'preserved_state_changed';
|
||||
end if;
|
||||
|
||||
if exists (select 1 from public.chat_sessions value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.chart_profiles value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.synastry_reports value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_cases value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_v4_cases value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_v4_jobs value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_agent_runs value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_diagnostics value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_public_messages value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (select 1 from public.birth_time_rectification_pending_evidence value join reset_snapshot snapshot on value.user_id = snapshot.id)
|
||||
or exists (
|
||||
select 1 from public.profiles profile join reset_snapshot snapshot on profile.id = snapshot.id
|
||||
where profile.name is not null or profile.birth_date is not null or profile.birth_time is not null
|
||||
or profile.country_code is not null or profile.province_code is not null or profile.city_code is not null or profile.district_code is not null
|
||||
or profile.onboarding_payload is not null or profile.onboarding_version is not null or profile.onboarding_generated_at is not null
|
||||
or profile.latitude is not null or profile.longitude is not null or profile.timezone_offset is not null
|
||||
or profile.reported_birth_time is not null or profile.active_birth_time is not null or profile.birth_time_source is not null
|
||||
or profile.birth_time_period is not null or profile.birth_time_clue is not null
|
||||
or profile.uncertainty_before_minutes is not null or profile.uncertainty_after_minutes is not null
|
||||
or profile.birth_time_status is not null or profile.rectification_confidence is not null or profile.rectification_case_id is not null
|
||||
or profile.birth_place_label is not null or profile.birth_place_type is not null or profile.birth_place_provider is not null
|
||||
or profile.birth_place_provider_id is not null or profile.timezone_id is not null or profile.timezone_source is not null
|
||||
) then
|
||||
raise exception 'reset_state_not_empty';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
commit;
|
||||
SQL
|
||||
|
||||
run_psql <<'SQL'
|
||||
begin;
|
||||
|
||||
create temporary table postflight_target on commit drop as
|
||||
select identity_user.id, identity_user.email, profile.credits,
|
||||
not (
|
||||
profile.name is null and profile.birth_date is null and profile.birth_time is null
|
||||
and profile.country_code is null and profile.province_code is null and profile.city_code is null and profile.district_code is null
|
||||
and profile.onboarding_payload is null and profile.onboarding_version is null and profile.onboarding_generated_at is null
|
||||
and profile.latitude is null and profile.longitude is null and profile.timezone_offset is null
|
||||
and profile.reported_birth_time is null and profile.active_birth_time is null and profile.birth_time_source is null
|
||||
and profile.birth_time_period is null and profile.birth_time_clue is null
|
||||
and profile.uncertainty_before_minutes is null and profile.uncertainty_after_minutes is null
|
||||
and profile.birth_time_status is null and profile.rectification_confidence is null and profile.rectification_case_id is null
|
||||
and profile.birth_place_label is null and profile.birth_place_type is null and profile.birth_place_provider is null
|
||||
and profile.birth_place_provider_id is null and profile.timezone_id is null and profile.timezone_source is null
|
||||
) as profile_not_reset,
|
||||
lower(btrim(profile.email)) = lower(btrim(identity_user.email)) as profile_email_matches
|
||||
from identity.users identity_user
|
||||
join auth.users auth_user on auth_user.id = identity_user.id
|
||||
and lower(btrim(auth_user.email)) = lower(btrim(identity_user.email))
|
||||
join public.profiles profile on profile.id = identity_user.id
|
||||
where lower(btrim(identity_user.email)) = lower(btrim(:'target_email'));
|
||||
|
||||
do $$
|
||||
declare
|
||||
target_id uuid;
|
||||
begin
|
||||
if (select count(*) from postflight_target) <> 1 then
|
||||
raise exception 'postflight_account_not_found_or_identity_bridge_mismatch';
|
||||
end if;
|
||||
select id into target_id from postflight_target;
|
||||
|
||||
if exists (select 1 from public.chat_sessions value where value.user_id = target_id)
|
||||
or exists (select 1 from public.chart_profiles value where value.user_id = target_id)
|
||||
or exists (select 1 from public.synastry_reports value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_cases value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_v4_cases value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_v4_jobs value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_agent_runs value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_diagnostics value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_public_messages value where value.user_id = target_id)
|
||||
or exists (select 1 from public.birth_time_rectification_pending_evidence value where value.user_id = target_id)
|
||||
or exists (select 1 from postflight_target where profile_not_reset) then
|
||||
raise exception 'postflight_reset_state_not_empty';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
select jsonb_build_object(
|
||||
'stage', 'postflight',
|
||||
'matchedAccounts', (select count(*) from postflight_target),
|
||||
'email', (select email from postflight_target),
|
||||
'credits', (select credits from postflight_target),
|
||||
'profileEmailMatches', (select profile_email_matches from postflight_target),
|
||||
'profileNotReset', (select profile_not_reset from postflight_target),
|
||||
'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = (select id from postflight_target)),
|
||||
'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = (select id from postflight_target)),
|
||||
'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = (select id from postflight_target)),
|
||||
'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = (select id from postflight_target)),
|
||||
'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = (select id from postflight_target)),
|
||||
'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = (select id from postflight_target)),
|
||||
'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = (select id from postflight_target)),
|
||||
'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = (select id from postflight_target)),
|
||||
'v5FeatureSnapshots', (select count(*) from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = (select id from postflight_target)),
|
||||
'v5PublicMessages', (select count(*) from public.birth_time_rectification_public_messages value where value.user_id = (select id from postflight_target)),
|
||||
'v5PendingEvidence', (select count(*) from public.birth_time_rectification_pending_evidence value where value.user_id = (select id from postflight_target)),
|
||||
'identityAccounts', (select count(*) from identity.accounts value where value.user_id = (select id from postflight_target)),
|
||||
'identitySessions', (select count(*) from identity.sessions value where value.user_id = (select id from postflight_target)),
|
||||
'creditTransactions', (select count(*) from public.credit_transactions value where value.user_id = (select id from postflight_target)),
|
||||
'creditCancellations', (select count(*) from public.credit_request_cancellations value where value.user_id = (select id from postflight_target)),
|
||||
'consultationRequests', (select count(*) from public.consultation_requests value where value.user_id = (select id from postflight_target)),
|
||||
'rectificationBilling', (select count(*) from public.birth_time_rectification_billing value where value.user_id = (select id from postflight_target)),
|
||||
'actionReceipts', (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = (select id from postflight_target))
|
||||
);
|
||||
|
||||
commit;
|
||||
SQL
|
||||
Reference in New Issue
Block a user