Import provider credentials without exposing secrets
This commit is contained in:
parent
6582b61dc4
commit
ca766dc1d3
|
|
@ -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,
|
||||
|
|
|
|||
66
scripts/import-fcm-service-account.sh
Executable file
66
scripts/import-fcm-service-account.sh
Executable 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."
|
||||
64
scripts/import-firebase-android-config.sh
Executable file
64
scripts/import-firebase-android-config.sh
Executable 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."
|
||||
67
scripts/import-google-oauth-client.sh
Executable file
67
scripts/import-google-oauth-client.sh
Executable 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."
|
||||
|
|
@ -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
97
scripts/set-env-values.sh
Executable 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"
|
||||
Loading…
Reference in New Issue
Block a user