fix(release): validate Firebase project consistency

This commit is contained in:
SimpleTest 2026-07-24 02:23:11 +03:00
parent 89851097fd
commit 0b9589f4fa
6 changed files with 217 additions and 16 deletions

View File

@ -38,6 +38,12 @@ edit, branch, or tag was made during this audit.
development blockers are the four public Firebase Android values and the FCM
service-account credential. No release-readiness claim is made until those
credentials are imported and provider/device behavior is exercised.
- The existing Google Cloud project `who-need-help-development` was selected in
the Firebase console through the user's already authenticated dev-port Chrome
session. Firebase requires the account holder to accept its Terms before it
can add Firebase services to that existing project. That legal acceptance
remains pending explicit user confirmation; no Firebase project, app,
service account, credential, or environment value was created or changed.
- Real browser Web Push was exercised on the development origin through the
user's existing dev-port Chrome profile. The exact origin permission was
changed from `Ask (default)` to `Allow`; the application registered a second,
@ -58,7 +64,7 @@ edit, branch, or tag was made during this audit.
topology, external PostgreSQL 18.4, healthy application containers, and
passing public readiness. The plan correctly refused release because the
production checkout still lacks browser VAPID, the Firebase Android client,
server FCM delivery, and Android App Links. It reported 46 pending local
server FCM delivery, and Android App Links. It reported 57 pending local
commits and made no remote change.
- A separate read-only isolation check observed the public Git `main` reference
still at production commit `921e04b3608007675e22e7e26e0beb3975dbba58`.
@ -82,6 +88,13 @@ edit, branch, or tag was made during this audit.
the database, test, public Git, and Devpost are excluded. An isolated offline
fixture passed read-only plan, successful apply, and injected-edge-failure
recovery, including restoration of the original image selection.
- The clean local commit
`89851097fd5cbe58ce4dc41c2322810894cad50a` was packaged as a Git bundle under
`output/releases/89851097fd5cbe58ce4dc41c2322810894cad50a/`. Its SHA-256
checksum, bundle object graph, and `HEAD` identity all passed verification.
The isolated production rollback drill was repeated after packaging and again
passed plan, apply, and injected edge-failure recovery without contacting or
changing the production runtime.
- A current development database backup was created at
`output/backups/compose-20260723-194923.dump` with SHA-256
`4202a152751d588c19069eb46a25753901238729b47e6197945afdca20b65c2e`.

View File

@ -79,6 +79,30 @@ valid_fcm_service_account_json() {
' >/dev/null 2>&1
}
fcm_service_account_project_id() {
jq -er '
select(
.type == "service_account" and
(.project_id | type == "string" and length > 0) and
(.client_email | type == "string" and length > 0) and
(.private_key | type == "string" and length > 0)
)
| .project_id
' 2>/dev/null
}
firebase_client_values_valid() {
local application_id sender_id prefix
application_id=$(value WNH_FIREBASE_APPLICATION_ID)
sender_id=$(value WNH_FIREBASE_GCM_SENDER_ID)
prefix="1:$sender_id:android:"
[[ "$sender_id" =~ ^[0-9]+$ &&
"$application_id" == "$prefix"* &&
-n "${application_id#"$prefix"}" ]]
}
failures=0
warnings=0
@ -174,38 +198,65 @@ if all_empty WNH_FIREBASE_APPLICATION_ID WNH_FIREBASE_API_KEY \
missing "Android Firebase client" "four WNH_FIREBASE_* Android client values"
elif all_set WNH_FIREBASE_APPLICATION_ID WNH_FIREBASE_API_KEY \
WNH_FIREBASE_PROJECT_ID WNH_FIREBASE_GCM_SENDER_ID; then
ready "Android Firebase client" "complete client configuration"
if firebase_client_values_valid; then
ready "Android Firebase client" "complete internally consistent client configuration"
else
invalid "Android Firebase client" \
"application ID must belong to the numeric configured sender/project number"
fi
else
partial "Android Firebase client" "all four WNH_FIREBASE_* values are required together"
fi
fcm_file=$(value FCM_SERVICE_ACCOUNT_FILE)
fcm_base64=$(value FCM_SERVICE_ACCOUNT_JSON_BASE64)
if [[ -z "$(value FCM_PROJECT_ID)" && -z "$fcm_file" && -z "$fcm_base64" ]]; then
fcm_project_id=$(value FCM_PROJECT_ID)
firebase_project_id=$(value WNH_FIREBASE_PROJECT_ID)
fcm_credential_project_id=
if [[ -z "$fcm_project_id" && -z "$fcm_file" && -z "$fcm_base64" ]]; then
missing "Android FCM delivery" "FCM project ID and one service-account source"
elif [[ -z "$(value FCM_PROJECT_ID)" || (-n "$fcm_file" && -n "$fcm_base64") ||
elif [[ -z "$fcm_project_id" || (-n "$fcm_file" && -n "$fcm_base64") ||
(-z "$fcm_file" && -z "$fcm_base64") ]]; then
partial "Android FCM delivery" "project ID and exactly one credential source are required"
elif [[ -n "$fcm_file" ]]; then
if [[ "$fcm_file" == /* && -r "$fcm_file" ]] &&
valid_fcm_service_account_json <"$fcm_file"; then
ready "Android FCM delivery" "complete service-account file is configured"
fcm_credential_project_id=$(fcm_service_account_project_id <"$fcm_file") &&
[[ "$fcm_credential_project_id" == "$fcm_project_id" ]] &&
[[ -z "$firebase_project_id" || "$fcm_project_id" == "$firebase_project_id" ]]; then
ready "Android FCM delivery" "service account and Android client use the same project"
else
invalid "Android FCM delivery" \
"FCM_SERVICE_ACCOUNT_FILE must be an absolute readable complete service-account JSON file"
"credential source and configured Firebase/FCM project IDs are incomplete or inconsistent"
fi
elif printf '%s' "$fcm_base64" | base64 --decode 2>/dev/null |
valid_fcm_service_account_json; then
ready "Android FCM delivery" "complete Base64 service-account document is configured"
elif fcm_credential_project_id=$(
printf '%s' "$fcm_base64" |
base64 --decode 2>/dev/null |
fcm_service_account_project_id
) &&
[[ "$fcm_credential_project_id" == "$fcm_project_id" ]] &&
[[ -z "$firebase_project_id" || "$fcm_project_id" == "$firebase_project_id" ]]; then
ready "Android FCM delivery" "service account and Android client use the same project"
else
invalid "Android FCM delivery" \
"Base64 credential is not a complete service-account JSON document"
"credential source and configured Firebase/FCM project IDs are incomplete or inconsistent"
fi
expected_android_package=
case "$deployment_env" in
development) expected_android_package=org.whoneedhelp.mobile.development ;;
test) expected_android_package=org.whoneedhelp.mobile.staging ;;
production) expected_android_package=org.whoneedhelp.mobile ;;
esac
if all_empty ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS; then
missing "Android App Links" "package name and signing certificate fingerprint"
elif all_set ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS; then
ready "Android App Links" "package and signing fingerprints are configured"
if [[ -n "$expected_android_package" &&
"$(value ANDROID_APP_LINKS_PACKAGE_NAME)" == "$expected_android_package" ]]; then
ready "Android App Links" "package and signing fingerprints match this environment"
else
invalid "Android App Links" "package does not match DEPLOYMENT_ENV=$deployment_env"
fi
else
partial "Android App Links" "package and signing fingerprints must be configured together"
fi

View File

@ -31,6 +31,8 @@ if [ -z "$package_name" ]; then
fi
if ! jq --exit-status --arg package "$package_name" '
(.project_info.project_number) as $project_number
|
[
.client[]?
| select(.client_info.android_client_info.package_name == $package)
@ -39,6 +41,8 @@ if ! jq --exit-status --arg package "$package_name" '
and (.project_info.project_id | type == "string" and length > 0)
and (.project_info.project_number | type == "string" and length > 0)
and ($clients[0].client_info.mobilesdk_app_id | type == "string" and length > 0)
and ($clients[0].client_info.mobilesdk_app_id
| startswith("1:" + $project_number + ":android:"))
and ($clients[0].api_key[0].current_key | type == "string" and length > 0)
' "$client_file" >/dev/null; then
echo "Firebase Android configuration does not contain exactly one complete client for package $package_name." >&2

View File

@ -202,6 +202,16 @@ grep -Fx 'WNH_FIREBASE_API_KEY=quality-firebase-api-key' "$credential_env" >/dev
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-development' "$credential_env" >/dev/null
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$credential_env" >/dev/null
firebase_mismatched_client="$scan_dir/google-services-mismatched.json"
printf '%s\n' \
'{"project_info":{"project_number":"123456789","project_id":"quality-development"},"client":[{"client_info":{"mobilesdk_app_id":"1:987654321:android:quality","android_client_info":{"package_name":"org.whoneedhelp.mobile.staging"}},"api_key":[{"current_key":"quality-firebase-api-key"}]}]}' \
>"$firebase_mismatched_client"
if ./scripts/import-firebase-android-config.sh \
"$credential_env" "$firebase_mismatched_client" >/dev/null 2>&1; then
echo "Firebase importer accepted an application ID from another project number." >&2
exit 1
fi
echo "Checking Android environment isolation"
android_fingerprint=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF
android_env="$scan_dir/android-development.env"
@ -399,6 +409,60 @@ if ./scripts/check-environment-readiness.sh \
echo "Environment readiness accepted an incomplete FCM service account." >&2
exit 1
fi
mismatched_firebase_env="$scan_dir/production.mismatched-firebase.env"
cp "$production_env" "$mismatched_firebase_env"
sed -i \
's|^WNH_FIREBASE_APPLICATION_ID=.*|WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality|' \
"$mismatched_firebase_env"
if ./scripts/validate-production-env.sh \
"$mismatched_firebase_env" help.test >/dev/null 2>&1; then
echo "Production validation accepted a Firebase application from another sender." >&2
exit 1
fi
if ./scripts/check-environment-readiness.sh \
"$mismatched_firebase_env" --require-release >/dev/null 2>&1; then
echo "Environment readiness accepted a Firebase application from another sender." >&2
exit 1
fi
mismatched_fcm_env="$scan_dir/production.mismatched-fcm.env"
mismatched_fcm_base64=$(
printf '%s' \
'{"type":"service_account","project_id":"another-project","client_email":"quality-fcm@another-project.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
base64 -w 0
)
cp "$production_env" "$mismatched_fcm_env"
sed -i \
"s|^FCM_SERVICE_ACCOUNT_JSON_BASE64=.*|FCM_SERVICE_ACCOUNT_JSON_BASE64=$mismatched_fcm_base64|" \
"$mismatched_fcm_env"
if ./scripts/validate-production-env.sh \
"$mismatched_fcm_env" help.test >/dev/null 2>&1; then
echo "Production validation accepted an FCM service account from another project." >&2
exit 1
fi
if ./scripts/check-environment-readiness.sh \
"$mismatched_fcm_env" --require-release >/dev/null 2>&1; then
echo "Environment readiness accepted an FCM service account from another project." >&2
exit 1
fi
mismatched_app_links_env="$scan_dir/production.mismatched-app-links.env"
cp "$production_env" "$mismatched_app_links_env"
sed -i \
's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
"$mismatched_app_links_env"
if ./scripts/validate-production-env.sh \
"$mismatched_app_links_env" help.test >/dev/null 2>&1; then
echo "Production validation accepted the staging Android package." >&2
exit 1
fi
if ./scripts/check-environment-readiness.sh \
"$mismatched_app_links_env" --require-release >/dev/null 2>&1; then
echo "Environment readiness accepted the staging Android package for production." >&2
exit 1
fi
grep -Fx 'COMPOSE_PROJECT_NAME=who_need_help_production' "$production_env" >/dev/null
grep -E '^APP_IMAGE=who-need-help:production-[0-9a-f]{12}$' "$production_env" >/dev/null
grep -Fx 'EDGE_COMPOSE_PROJECT_NAME=who_need_help_edge' "$production_env" >/dev/null

View File

@ -351,6 +351,15 @@ if ((firebase_nonempty != 0 && firebase_nonempty != ${#firebase_values[@]})); th
echo "All four WNH_FIREBASE_* Android client values must be configured together." >&2
exit 1
fi
if ((firebase_nonempty == ${#firebase_values[@]})); then
firebase_prefix="1:$firebase_sender_id:android:"
[[ "$firebase_sender_id" =~ ^[0-9]+$ &&
"$firebase_application_id" == "$firebase_prefix"* &&
-n "${firebase_application_id#"$firebase_prefix"}" ]] || {
echo "WNH_FIREBASE_APPLICATION_ID does not belong to WNH_FIREBASE_GCM_SENDER_ID." >&2
exit 1
}
fi
vapid_values=(
"$web_push_vapid_public_key"
@ -386,6 +395,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" ||
exit 1
}
fcm_credential_project_id=
if [[ -n "$fcm_service_account_file" ]]; then
command -v jq >/dev/null 2>&1 || {
echo "Required command is unavailable for FCM validation: jq" >&2
@ -399,6 +409,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" ||
echo "FCM_SERVICE_ACCOUNT_FILE is not a complete service-account JSON document." >&2
exit 1
}
fcm_credential_project_id=$(jq --raw-output '.project_id' "$fcm_service_account_file")
else
for command in base64 jq; do
command -v "$command" >/dev/null 2>&1 || {
@ -406,12 +417,30 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" ||
exit 1
}
done
printf '%s' "$fcm_service_account_json_base64" |
base64 --decode 2>/dev/null |
fcm_decoded_json=$(
printf '%s' "$fcm_service_account_json_base64" |
base64 --decode 2>/dev/null
) || {
echo "FCM_SERVICE_ACCOUNT_JSON_BASE64 is not valid Base64." >&2
exit 1
}
printf '%s' "$fcm_decoded_json" |
valid_fcm_service_account_json || {
echo "FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a complete service-account JSON document." >&2
exit 1
}
fcm_credential_project_id=$(printf '%s' "$fcm_decoded_json" | jq --raw-output '.project_id')
fi
[[ "$fcm_credential_project_id" == "$fcm_project_id" ]] || {
echo "FCM service-account project does not match FCM_PROJECT_ID." >&2
exit 1
}
if ((firebase_nonempty == ${#firebase_values[@]})); then
[[ "$fcm_project_id" == "$firebase_project_id" ]] || {
echo "FCM_PROJECT_ID does not match WNH_FIREBASE_PROJECT_ID." >&2
exit 1
}
fi
fi
@ -424,6 +453,10 @@ if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprint
echo "ANDROID_APP_LINKS_PACKAGE_NAME is not a valid Android application ID." >&2
exit 1
}
[[ "$android_app_links_package_name" == org.whoneedhelp.mobile ]] || {
echo "Production Android App Links must use org.whoneedhelp.mobile." >&2
exit 1
}
IFS=',' read -r -a android_fingerprints <<<"$android_app_links_fingerprints"
[[ ${#android_fingerprints[@]} -gt 0 ]] || {

View File

@ -147,6 +147,18 @@ if ((firebase_nonempty != 0 && firebase_nonempty != ${#firebase_values[@]})); th
echo "All four test WNH_FIREBASE_* Android client values must be configured together." >&2
exit 1
fi
if ((firebase_nonempty == ${#firebase_values[@]})); then
firebase_application_id=${firebase_values[0]}
firebase_project_id=${firebase_values[2]}
firebase_sender_id=${firebase_values[3]}
firebase_prefix="1:$firebase_sender_id:android:"
[[ "$firebase_sender_id" =~ ^[0-9]+$ &&
"$firebase_application_id" == "$firebase_prefix"* &&
-n "${firebase_application_id#"$firebase_prefix"}" ]] || {
echo "Test WNH_FIREBASE_APPLICATION_ID does not belong to WNH_FIREBASE_GCM_SENDER_ID." >&2
exit 1
}
fi
vapid_values=(
"$(read_value WEB_PUSH_VAPID_PUBLIC_KEY 2>/dev/null || true)"
@ -184,6 +196,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" ||
exit 1
}
fcm_credential_project_id=
if [[ -n "$fcm_service_account_file" ]]; then
command -v jq >/dev/null 2>&1 || {
echo "Required command is unavailable for FCM validation: jq" >&2
@ -197,6 +210,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" ||
echo "Test FCM_SERVICE_ACCOUNT_FILE is not a complete service-account JSON document." >&2
exit 1
}
fcm_credential_project_id=$(jq --raw-output '.project_id' "$fcm_service_account_file")
else
for command in base64 jq; do
command -v "$command" >/dev/null 2>&1 || {
@ -204,12 +218,30 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" ||
exit 1
}
done
printf '%s' "$fcm_service_account_json_base64" |
base64 --decode 2>/dev/null |
fcm_decoded_json=$(
printf '%s' "$fcm_service_account_json_base64" |
base64 --decode 2>/dev/null
) || {
echo "Test FCM_SERVICE_ACCOUNT_JSON_BASE64 is not valid Base64." >&2
exit 1
}
printf '%s' "$fcm_decoded_json" |
valid_fcm_service_account_json || {
echo "Test FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a complete service-account JSON document." >&2
exit 1
}
fcm_credential_project_id=$(printf '%s' "$fcm_decoded_json" | jq --raw-output '.project_id')
fi
[[ "$fcm_credential_project_id" == "$fcm_project_id" ]] || {
echo "Test FCM service-account project does not match FCM_PROJECT_ID." >&2
exit 1
}
if ((firebase_nonempty == ${#firebase_values[@]})); then
[[ "$fcm_project_id" == "$firebase_project_id" ]] || {
echo "Test FCM_PROJECT_ID does not match WNH_FIREBASE_PROJECT_ID." >&2
exit 1
}
fi
fi
@ -224,6 +256,10 @@ if [[ -n "$android_package" || -n "$android_fingerprints" ]]; then
echo "ANDROID_APP_LINKS_PACKAGE_NAME is not a valid Android application ID." >&2
exit 1
}
[[ "$android_package" == org.whoneedhelp.mobile.staging ]] || {
echo "Test Android App Links must use org.whoneedhelp.mobile.staging." >&2
exit 1
}
IFS=',' read -r -a android_fingerprint_values <<<"$android_fingerprints"
for fingerprint in "${android_fingerprint_values[@]}"; do