From 6582b61dc4faa1221edbf6c64fc81b4d2a8ff4fa Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Thu, 23 Jul 2026 20:51:51 +0300 Subject: [PATCH] Validate FCM service account credentials --- config/runtime.exs | 8 ++++-- scripts/check-environment-readiness.sh | 24 ++++++++++++----- scripts/init-production-env.sh | 20 ++++++++++++++ scripts/init-test-env.sh | 20 ++++++++++++++ scripts/quality.sh | 22 ++++++++++++++- scripts/validate-production-env.sh | 22 ++++++++++++--- scripts/validate-test-env.sh | 37 ++++++++++++++++++++++++++ 7 files changed, 141 insertions(+), 12 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index de03c98..7939907 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -332,8 +332,12 @@ fcm_credentials = raise "Set only one of FCM_SERVICE_ACCOUNT_FILE or FCM_SERVICE_ACCOUNT_JSON_BASE64." end -if fcm_credentials && fcm_credentials["type"] != "service_account" do - raise "The configured FCM credentials must be a Google service-account document." +if fcm_credentials && + (fcm_credentials["type"] != "service_account" || + Enum.any?(["project_id", "client_email", "private_key"], fn field -> + not is_binary(fcm_credentials[field]) or fcm_credentials[field] == "" + end)) do + raise "The configured FCM credentials must be a complete Google service-account document." end fcm_configuration = diff --git a/scripts/check-environment-readiness.sh b/scripts/check-environment-readiness.sh index 6043bfd..a0da9d1 100755 --- a/scripts/check-environment-readiness.sh +++ b/scripts/check-environment-readiness.sh @@ -70,6 +70,15 @@ contains_template_marker() { "$observed" == *example.com* || "$observed" == *example.invalid* ]] } +valid_fcm_service_account_json() { + jq -e ' + .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) + ' >/dev/null 2>&1 +} + failures=0 warnings=0 @@ -178,16 +187,19 @@ elif [[ -z "$(value 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" ]]; then - ready "Android FCM delivery" "readable service-account file is configured" + if [[ "$fcm_file" == /* && -r "$fcm_file" ]] && + valid_fcm_service_account_json <"$fcm_file"; then + ready "Android FCM delivery" "complete service-account file is configured" else - invalid "Android FCM delivery" "FCM_SERVICE_ACCOUNT_FILE must be an absolute readable file" + invalid "Android FCM delivery" \ + "FCM_SERVICE_ACCOUNT_FILE must be an absolute readable complete service-account JSON file" fi elif printf '%s' "$fcm_base64" | base64 --decode 2>/dev/null | - jq -e '.type == "service_account" and (.project_id | type == "string")' >/dev/null 2>&1; then - ready "Android FCM delivery" "valid Base64 service-account document is configured" + valid_fcm_service_account_json; then + ready "Android FCM delivery" "complete Base64 service-account document is configured" else - invalid "Android FCM delivery" "Base64 credential is not a service-account JSON document" + invalid "Android FCM delivery" \ + "Base64 credential is not a complete service-account JSON document" fi if all_empty ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS; then diff --git a/scripts/init-production-env.sh b/scripts/init-production-env.sh index 7cba8c4..8a16591 100755 --- a/scripts/init-production-env.sh +++ b/scripts/init-production-env.sh @@ -114,6 +114,26 @@ if { [ -n "$fcm_project_id" ] || [ -n "$fcm_service_account_json_base64" ]; } && exit 1 fi +if [ -n "$fcm_service_account_json_base64" ]; then + for command in base64 jq; do + command -v "$command" >/dev/null 2>&1 || { + echo "Required command is unavailable for FCM validation: $command" >&2 + exit 1 + } + done + if ! printf '%s' "$fcm_service_account_json_base64" | + base64 --decode 2>/dev/null | + jq -e ' + .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) + ' >/dev/null 2>&1; then + echo "Production FCM credential is not a complete service-account JSON document." >&2 + exit 1 + fi +fi + case "$compose_project_name" in *[!a-zA-Z0-9_-]* | '') echo "PRODUCTION_COMPOSE_PROJECT_NAME must contain only letters, numbers, underscores, or hyphens." >&2 diff --git a/scripts/init-test-env.sh b/scripts/init-test-env.sh index 6e697f1..84d2a87 100755 --- a/scripts/init-test-env.sh +++ b/scripts/init-test-env.sh @@ -125,6 +125,26 @@ if [[ (-n "$fcm_project_id" || -n "$fcm_service_account_json_base64") && exit 1 fi +if [[ -n "$fcm_service_account_json_base64" ]]; then + for command in base64 jq; do + command -v "$command" >/dev/null 2>&1 || { + echo "Required command is unavailable for FCM validation: $command" >&2 + exit 1 + } + done + if ! printf '%s' "$fcm_service_account_json_base64" | + base64 --decode 2>/dev/null | + jq -e ' + .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) + ' >/dev/null 2>&1; then + echo "Test FCM credential is not a complete service-account JSON document." >&2 + exit 1 + fi +fi + for pair in "HTTP:$http_port" "MAILPIT:$mailpit_port"; do value=${pair#*:} if [[ ! "$value" =~ ^[0-9]+$ ]] || ((value < 1 || value > 65535)); then diff --git a/scripts/quality.sh b/scripts/quality.sh index 306d8fe..d2d9e64 100755 --- a/scripts/quality.sh +++ b/scripts/quality.sh @@ -117,7 +117,8 @@ test "$(sha256sum "$legacy_load_env" | awk '{print $1}')" = "$legacy_load_hash" echo "Checking independent test and production environment initialization" quality_fcm_base64=$( - printf '%s' '{"type":"service_account","project_id":"quality-production"}' | + printf '%s' \ + '{"type":"service_account","project_id":"quality-production","client_email":"quality-fcm@quality-production.iam.gserviceaccount.com","private_key":"quality-private-key"}' | base64 -w 0 ) test_env="$scan_dir/test.env" @@ -206,6 +207,25 @@ PRODUCTION_SUPPORT_INBOX_ADDRESS=support@help.test \ test "$(stat -c '%a' "$production_env")" = 600 ./scripts/validate-production-env.sh "$production_env" help.test >/dev/null ./scripts/check-environment-readiness.sh "$production_env" --require-release >/dev/null +invalid_fcm_env="$scan_dir/production.invalid-fcm.env" +invalid_fcm_base64=$( + printf '%s' '{"type":"service_account","project_id":"quality-production"}' | + base64 -w 0 +) +cp "$production_env" "$invalid_fcm_env" +sed -i \ + "s|^FCM_SERVICE_ACCOUNT_JSON_BASE64=.*|FCM_SERVICE_ACCOUNT_JSON_BASE64=$invalid_fcm_base64|" \ + "$invalid_fcm_env" +if ./scripts/validate-production-env.sh \ + "$invalid_fcm_env" help.test >/dev/null 2>&1; then + echo "Production validation accepted an incomplete FCM service account." >&2 + exit 1 +fi +if ./scripts/check-environment-readiness.sh \ + "$invalid_fcm_env" --require-release >/dev/null 2>&1; then + echo "Environment readiness accepted an incomplete FCM service account." >&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 diff --git a/scripts/validate-production-env.sh b/scripts/validate-production-env.sh index 1229cdd..bfa1856 100755 --- a/scripts/validate-production-env.sh +++ b/scripts/validate-production-env.sh @@ -59,6 +59,15 @@ require_value() { printf '%s' "$value" } +valid_fcm_service_account_json() { + jq -e ' + .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) + ' >/dev/null 2>&1 +} + reject_marker() { local key=$1 local value=$2 @@ -378,10 +387,18 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || } if [[ -n "$fcm_service_account_file" ]]; then + command -v jq >/dev/null 2>&1 || { + echo "Required command is unavailable for FCM validation: jq" >&2 + exit 1 + } [[ "$fcm_service_account_file" == /* && -r "$fcm_service_account_file" ]] || { echo "FCM_SERVICE_ACCOUNT_FILE must be an absolute readable file." >&2 exit 1 } + valid_fcm_service_account_json <"$fcm_service_account_file" || { + echo "FCM_SERVICE_ACCOUNT_FILE is not a complete service-account JSON document." >&2 + exit 1 + } else for command in base64 jq; do command -v "$command" >/dev/null 2>&1 || { @@ -391,9 +408,8 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || done printf '%s' "$fcm_service_account_json_base64" | base64 --decode 2>/dev/null | - jq -e '.type == "service_account" and (.project_id | type == "string")' \ - >/dev/null 2>&1 || { - echo "FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a service-account JSON document." >&2 + valid_fcm_service_account_json || { + echo "FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a complete service-account JSON document." >&2 exit 1 } fi diff --git a/scripts/validate-test-env.sh b/scripts/validate-test-env.sh index 7e91615..ddad2d1 100755 --- a/scripts/validate-test-env.sh +++ b/scripts/validate-test-env.sh @@ -41,6 +41,15 @@ require_value() { printf '%s' "$value" } +valid_fcm_service_account_json() { + jq -e ' + .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) + ' >/dev/null 2>&1 +} + [[ "$(require_value DEPLOYMENT_ENV)" == test ]] || { echo "Test validation requires DEPLOYMENT_ENV=test." >&2 exit 1 @@ -174,6 +183,34 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || echo "Test FCM project ID and exactly one credential source are required together." >&2 exit 1 } + + if [[ -n "$fcm_service_account_file" ]]; then + command -v jq >/dev/null 2>&1 || { + echo "Required command is unavailable for FCM validation: jq" >&2 + exit 1 + } + [[ "$fcm_service_account_file" == /* && -r "$fcm_service_account_file" ]] || { + echo "Test FCM_SERVICE_ACCOUNT_FILE must be an absolute readable file." >&2 + exit 1 + } + valid_fcm_service_account_json <"$fcm_service_account_file" || { + echo "Test FCM_SERVICE_ACCOUNT_FILE is not a complete service-account JSON document." >&2 + exit 1 + } + else + for command in base64 jq; do + command -v "$command" >/dev/null 2>&1 || { + echo "Required command is unavailable for FCM validation: $command" >&2 + exit 1 + } + done + printf '%s' "$fcm_service_account_json_base64" | + base64 --decode 2>/dev/null | + valid_fcm_service_account_json || { + echo "Test FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a complete service-account JSON document." >&2 + exit 1 + } + fi fi android_package=$(read_value ANDROID_APP_LINKS_PACKAGE_NAME 2>/dev/null || true)