Import provider credentials without exposing secrets

This commit is contained in:
SimpleTest 2026-07-23 21:12:07 +03:00
parent 6582b61dc4
commit ca766dc1d3
6 changed files with 406 additions and 0 deletions

View File

@ -167,6 +167,29 @@ Check an environment without printing its secret values:
./scripts/check-environment-readiness.sh .env --require-release
```
Provider downloads can be imported into that same file without placing
secrets on a command line or printing them:
```bash
chmod 600 /secure/downloads/google-oauth-client.json
./scripts/import-google-oauth-client.sh \
.env /secure/downloads/google-oauth-client.json
./scripts/import-firebase-android-config.sh \
.env /secure/downloads/google-services.json
chmod 600 /secure/downloads/fcm-service-account.json
./scripts/import-fcm-service-account.sh \
.env /secure/downloads/fcm-service-account.json
```
The importers validate the exact OAuth callback, Android package, Firebase
project relationship, and required service-account fields before atomically
replacing existing keys. They preserve mode `0600` and never create another
permanent environment file. OAuth and service-account downloads still contain
private credentials after import; deliberately move them to protected backup
storage or remove them after verification.
The first command reports incomplete or local-only capabilities. The second is
a blocking release preflight and exits nonzero until Google sign-in, external
SMTP, browser Web Push, Android Firebase/FCM, App Links, support routing,

View File

@ -0,0 +1,66 @@
#!/bin/sh
set -eu
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
if [ "$#" -ne 2 ]; then
echo "Usage: $0 ENV_FILE FCM_SERVICE_ACCOUNT_JSON" >&2
exit 1
fi
env_file=$1
service_account_file=$2
if [ ! -f "$service_account_file" ]; then
echo "FCM service-account JSON does not exist: $service_account_file" >&2
exit 1
fi
case "$(stat -c '%a' "$service_account_file")" in
400|600) ;;
*)
echo "FCM service-account JSON contains a private key and must have mode 0400 or 0600." >&2
exit 1
;;
esac
if ! jq --exit-status '
.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)
' "$service_account_file" >/dev/null; then
echo "FCM service-account JSON is incomplete." >&2
exit 1
fi
project_id=$(jq --raw-output '.project_id' "$service_account_file")
firebase_project_id=$(
awk -F= '
$1 == "WNH_FIREBASE_PROJECT_ID" {
print substr($0, index($0, "=") + 1)
exit
}
' "$env_file"
)
if [ -n "$firebase_project_id" ] && [ "$firebase_project_id" != "$project_id" ]; then
echo "FCM service-account project does not match WNH_FIREBASE_PROJECT_ID." >&2
exit 1
fi
values_file=$(mktemp "${TMPDIR:-/tmp}/wnh-fcm-service-account-values.XXXXXX")
trap 'rm -f "$values_file"' EXIT HUP INT TERM
chmod 600 "$values_file"
{
printf 'FCM_PROJECT_ID=%s\n' "$project_id"
printf 'FCM_SERVICE_ACCOUNT_FILE=\n'
printf 'FCM_SERVICE_ACCOUNT_JSON_BASE64='
base64 -w 0 "$service_account_file"
printf '\n'
} >"$values_file"
"$ROOT/scripts/set-env-values.sh" "$env_file" "$values_file" >/dev/null
echo "FCM service account imported without printing the private key."
echo "The downloaded JSON still contains the private key; store or remove it deliberately."

View File

@ -0,0 +1,64 @@
#!/bin/sh
set -eu
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
if [ "$#" -ne 2 ]; then
echo "Usage: $0 ENV_FILE GOOGLE_SERVICES_JSON" >&2
exit 1
fi
env_file=$1
client_file=$2
if [ ! -f "$client_file" ]; then
echo "Firebase Android configuration does not exist: $client_file" >&2
exit 1
fi
package_name=$(
awk -F= '
$1 == "ANDROID_APP_LINKS_PACKAGE_NAME" {
print substr($0, index($0, "=") + 1)
exit
}
' "$env_file"
)
if [ -z "$package_name" ]; then
echo "ANDROID_APP_LINKS_PACKAGE_NAME is missing from the selected environment." >&2
exit 1
fi
if ! jq --exit-status --arg package "$package_name" '
[
.client[]?
| select(.client_info.android_client_info.package_name == $package)
] as $clients
| ($clients | length == 1)
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].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
exit 1
fi
values_file=$(mktemp "${TMPDIR:-/tmp}/wnh-firebase-android-values.XXXXXX")
trap 'rm -f "$values_file"' EXIT HUP INT TERM
chmod 600 "$values_file"
jq --raw-output --arg package "$package_name" '
(
.client[]
| select(.client_info.android_client_info.package_name == $package)
) as $client
| "WNH_FIREBASE_APPLICATION_ID=\($client.client_info.mobilesdk_app_id)",
"WNH_FIREBASE_API_KEY=\($client.api_key[0].current_key)",
"WNH_FIREBASE_PROJECT_ID=\(.project_info.project_id)",
"WNH_FIREBASE_GCM_SENDER_ID=\(.project_info.project_number)"
' "$client_file" >"$values_file"
"$ROOT/scripts/set-env-values.sh" "$env_file" "$values_file" >/dev/null
echo "Public Firebase Android values imported for package $package_name."

View File

@ -0,0 +1,67 @@
#!/bin/sh
set -eu
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
if [ "$#" -ne 2 ]; then
echo "Usage: $0 ENV_FILE GOOGLE_OAUTH_CLIENT_JSON" >&2
exit 1
fi
env_file=$1
client_file=$2
if [ ! -f "$client_file" ]; then
echo "Google OAuth client JSON does not exist: $client_file" >&2
exit 1
fi
case "$(stat -c '%a' "$client_file")" in
400|600) ;;
*)
echo "Google OAuth client JSON contains a client secret and must have mode 0400 or 0600." >&2
exit 1
;;
esac
base_url=$(
awk -F= '
$1 == "WNH_BASE_URL" {
print substr($0, index($0, "=") + 1)
exit
}
' "$env_file"
)
if [ -z "$base_url" ]; then
echo "WNH_BASE_URL is missing from the selected environment." >&2
exit 1
fi
callback_url=${base_url%/}/auth/google/callback
if ! jq --exit-status --arg callback "$callback_url" '
.web as $web
| ($web | type == "object")
and ($web.client_id | type == "string" and length > 0 and test("^[^\r\n]+$"))
and ($web.client_secret | type == "string" and length > 0 and test("^[^\r\n]+$"))
and ($web.redirect_uris | type == "array")
and any($web.redirect_uris[]; . == $callback)
' "$client_file" >/dev/null; then
echo "Google OAuth JSON is incomplete or does not contain the exact callback: $callback_url" >&2
exit 1
fi
values_file=$(mktemp "${TMPDIR:-/tmp}/wnh-google-oauth-values.XXXXXX")
trap 'rm -f "$values_file"' EXIT HUP INT TERM
chmod 600 "$values_file"
jq --raw-output '
.web
| "GOOGLE_OAUTH_CLIENT_ID=\(.client_id)",
"GOOGLE_OAUTH_CLIENT_SECRET=\(.client_secret)"
' "$client_file" >"$values_file"
"$ROOT/scripts/set-env-values.sh" "$env_file" "$values_file" >/dev/null
echo "Google OAuth client imported without printing its ID or secret."
echo "The downloaded JSON still contains the client secret; store or remove it deliberately."

View File

@ -94,6 +94,95 @@ grep -Fx '/.runner' .dockerignore >/dev/null
grep -Fx '/act_runner' .dockerignore >/dev/null
grep -Fx '/act_runner-data/' .dockerignore >/dev/null
echo "Checking atomic environment credential imports"
credential_env="$scan_dir/credentials.env"
cp .env.example "$credential_env"
chmod 600 "$credential_env"
credential_values="$scan_dir/credential-values"
printf '%s\n' \
'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' \
'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' >"$credential_values"
chmod 600 "$credential_values"
credential_output=$(
./scripts/set-env-values.sh "$credential_env" "$credential_values"
)
if printf '%s' "$credential_output" | grep -F 'quality-imported-secret' >/dev/null; then
echo "Environment updater printed a secret value." >&2
exit 1
fi
test "$(stat -c '%a' "$credential_env")" = 600
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' "$credential_env" >/dev/null
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' "$credential_env" >/dev/null
duplicate_env="$scan_dir/credentials-duplicate.env"
cp "$credential_env" "$duplicate_env"
printf '%s\n' 'GOOGLE_OAUTH_CLIENT_ID=duplicate' >>"$duplicate_env"
if ./scripts/set-env-values.sh \
"$duplicate_env" "$credential_values" >/dev/null 2>&1; then
echo "Environment updater accepted a duplicate target key." >&2
exit 1
fi
google_client="$scan_dir/google-oauth-client.json"
printf '%s\n' \
'{"web":{"client_id":"quality-google-client","project_id":"quality-development","client_secret":"quality-google-secret","redirect_uris":["https://dev.help.test/auth/google/callback"]}}' \
>"$google_client"
chmod 600 "$google_client"
sed -i 's|^WNH_BASE_URL=.*|WNH_BASE_URL=https://dev.help.test|' "$credential_env"
oauth_output=$(
./scripts/import-google-oauth-client.sh "$credential_env" "$google_client"
)
if printf '%s' "$oauth_output" | grep -F 'quality-google-secret' >/dev/null; then
echo "Google OAuth importer printed a client secret." >&2
exit 1
fi
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-google-client' "$credential_env" >/dev/null
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-google-secret' "$credential_env" >/dev/null
firebase_client="$scan_dir/google-services.json"
printf '%s\n' \
'{"project_info":{"project_number":"123456789","project_id":"quality-development"},"client":[{"client_info":{"mobilesdk_app_id":"1:123456789:android:quality","android_client_info":{"package_name":"org.whoneedhelp.mobile.staging"}},"api_key":[{"current_key":"quality-firebase-api-key"}]}]}' \
>"$firebase_client"
sed -i \
's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
"$credential_env"
./scripts/import-firebase-android-config.sh \
"$credential_env" "$firebase_client" >/dev/null
grep -Fx 'WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality' "$credential_env" >/dev/null
grep -Fx 'WNH_FIREBASE_API_KEY=quality-firebase-api-key' "$credential_env" >/dev/null
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-development' "$credential_env" >/dev/null
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$credential_env" >/dev/null
fcm_service_account="$scan_dir/fcm-service-account.json"
printf '%s\n' \
'{"type":"service_account","project_id":"quality-development","client_email":"quality-fcm@quality-development.iam.gserviceaccount.com","private_key":"quality-private-key"}' \
>"$fcm_service_account"
chmod 600 "$fcm_service_account"
fcm_output=$(
./scripts/import-fcm-service-account.sh \
"$credential_env" "$fcm_service_account"
)
if printf '%s' "$fcm_output" | grep -F 'quality-private-key' >/dev/null; then
echo "FCM importer printed a private key." >&2
exit 1
fi
grep -Fx 'FCM_PROJECT_ID=quality-development' "$credential_env" >/dev/null
grep -Fx 'FCM_SERVICE_ACCOUNT_FILE=' "$credential_env" >/dev/null
credential_fcm_base64=$(
awk -F= '
$1 == "FCM_SERVICE_ACCOUNT_JSON_BASE64" {
print substr($0, index($0, "=") + 1)
exit
}
' "$credential_env"
)
printf '%s' "$credential_fcm_base64" |
base64 -d |
jq --exit-status \
'.project_id == "quality-development" and .private_key == "quality-private-key"' \
>/dev/null
echo "Checking the existing load environment upgrade path"
legacy_load_env="$scan_dir/legacy-load.env"
printf '%s\n' \

97
scripts/set-env-values.sh Executable file
View File

@ -0,0 +1,97 @@
#!/bin/sh
set -eu
if [ "$#" -ne 2 ]; then
echo "Usage: $0 ENV_FILE VALUES_FILE" >&2
exit 1
fi
env_file=$1
values_file=$2
if [ ! -f "$env_file" ]; then
echo "Environment file does not exist: $env_file" >&2
exit 1
fi
if [ ! -f "$values_file" ]; then
echo "Values file does not exist: $values_file" >&2
exit 1
fi
if [ "$(stat -c '%a' "$env_file")" != 600 ]; then
echo "Environment file must have mode 0600: $env_file" >&2
exit 1
fi
case "$(stat -c '%a' "$values_file")" in
400|600) ;;
*)
echo "Values file must have mode 0400 or 0600: $values_file" >&2
exit 1
;;
esac
env_dir=$(CDPATH='' cd -- "$(dirname -- "$env_file")" && pwd)
env_name=$(basename -- "$env_file")
temporary_env=$(mktemp "$env_dir/$env_name.tmp.XXXXXX")
trap 'rm -f "$temporary_env"' EXIT HUP INT TERM
chmod 600 "$temporary_env"
if ! awk '
BEGIN {
FS = "="
reading_values = 1
}
FNR == 1 && NR != 1 {
reading_values = 0
}
reading_values {
key = $1
if (key !~ /^[A-Z][A-Z0-9_]*$/ || key in replacement) {
exit 40
}
replacement[key] = substr($0, index($0, "=") + 1)
replacement_count++
next
}
{
separator = index($0, "=")
if (separator > 1) {
key = substr($0, 1, separator - 1)
if (key in replacement) {
seen[key]++
print key "=" replacement[key]
next
}
}
print
}
END {
if (replacement_count == 0) {
exit 41
}
for (key in replacement) {
if (seen[key] != 1) {
exit 42
}
}
}
' "$values_file" "$env_file" >"$temporary_env"; then
echo "Refusing to update the environment: values must use unique KEY=VALUE lines and every key must already occur exactly once." >&2
exit 1
fi
mv "$temporary_env" "$env_file"
trap - EXIT HUP INT TERM
echo "Environment values updated without printing their contents: $env_file"