175 lines
5.3 KiB
Bash
175 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Configure the GeoIntel operator login in .env.
|
|
#
|
|
# Writes GEOINTEL_AUTH_ENABLED, GEOINTEL_AUTH_USERNAME, a pbkdf2_sha256
|
|
# password hash and a fresh random session secret. The plaintext password is
|
|
# never written to disk, never printed and never passed as a command argument.
|
|
#
|
|
# Usage:
|
|
# bash scripts/configure_operator_login.sh --username ITWorx
|
|
# -> prompts for the password (nothing lands in shell history)
|
|
#
|
|
# GEOINTEL_OPERATOR_PASSWORD='...' bash scripts/configure_operator_login.sh \
|
|
# --username ITWorx --non-interactive
|
|
#
|
|
# Options:
|
|
# --username <value> Operator login name. Required.
|
|
# --env-file <path> Target env file. Default: .env
|
|
# --guest-access <bool> Keep the public guest demo. Default: false
|
|
# --non-interactive Read the password from GEOINTEL_OPERATOR_PASSWORD.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
USERNAME=""
|
|
ENV_FILE=".env"
|
|
GUEST_ACCESS="false"
|
|
NON_INTERACTIVE="false"
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--username) USERNAME="${2:-}"; shift 2 ;;
|
|
--env-file) ENV_FILE="${2:-}"; shift 2 ;;
|
|
--guest-access) GUEST_ACCESS="${2:-}"; shift 2 ;;
|
|
--non-interactive) NON_INTERACTIVE="true"; shift ;;
|
|
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
|
|
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$USERNAME" ]; then
|
|
echo "--username is required." >&2
|
|
exit 2
|
|
fi
|
|
|
|
case "$GUEST_ACCESS" in
|
|
true|false) ;;
|
|
*) echo "--guest-access must be true or false." >&2; exit 2 ;;
|
|
esac
|
|
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
if [ -f deploy/unraid/geointel.env.example ]; then
|
|
echo "Creating ${ENV_FILE} from deploy/unraid/geointel.env.example."
|
|
cp deploy/unraid/geointel.env.example "$ENV_FILE"
|
|
else
|
|
echo "${ENV_FILE} does not exist and no template was found." >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
if [ "$NON_INTERACTIVE" = "true" ]; then
|
|
if [ -z "${GEOINTEL_OPERATOR_PASSWORD:-}" ]; then
|
|
echo "GEOINTEL_OPERATOR_PASSWORD must be set for --non-interactive." >&2
|
|
exit 2
|
|
fi
|
|
else
|
|
printf 'Password for %s: ' "$USERNAME" >&2
|
|
read -r -s GEOINTEL_OPERATOR_PASSWORD
|
|
printf '\n' >&2
|
|
printf 'Repeat password: ' >&2
|
|
read -r -s password_repeat
|
|
printf '\n' >&2
|
|
if [ "$GEOINTEL_OPERATOR_PASSWORD" != "$password_repeat" ]; then
|
|
echo "Passwords do not match." >&2
|
|
exit 2
|
|
fi
|
|
unset password_repeat
|
|
fi
|
|
|
|
if [ "${#GEOINTEL_OPERATOR_PASSWORD}" -lt 12 ]; then
|
|
echo "Refusing to configure an operator password shorter than 12 characters." >&2
|
|
exit 2
|
|
fi
|
|
export GEOINTEL_OPERATOR_PASSWORD
|
|
|
|
HASH_SCRIPT="$(mktemp)"
|
|
trap 'rm -f "$HASH_SCRIPT"' EXIT
|
|
cat >"$HASH_SCRIPT" <<'PY'
|
|
import base64
|
|
import hashlib
|
|
import os
|
|
import secrets
|
|
|
|
# Must stay identical to AuthService.hash_password in
|
|
# backend/app/services/auth_service.py.
|
|
HASH_NAME = "pbkdf2_sha256"
|
|
ITERATIONS = 600_000
|
|
|
|
|
|
def b64(value: bytes) -> str:
|
|
return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
|
|
|
|
|
|
password = os.environ["GEOINTEL_OPERATOR_PASSWORD"]
|
|
salt = secrets.token_bytes(18)
|
|
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS)
|
|
|
|
print("$".join((HASH_NAME, str(ITERATIONS), b64(salt), b64(digest))))
|
|
print(secrets.token_urlsafe(48))
|
|
PY
|
|
|
|
generated=""
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
generated="$(python3 "$HASH_SCRIPT")"
|
|
elif command -v docker >/dev/null 2>&1; then
|
|
echo "No local python3 found; deriving the hash in a throwaway python container."
|
|
generated="$(
|
|
docker run --rm -i \
|
|
-e GEOINTEL_OPERATOR_PASSWORD \
|
|
python:3.11-slim python - <"$HASH_SCRIPT"
|
|
)"
|
|
else
|
|
echo "Need either python3 or docker to derive the password hash." >&2
|
|
exit 2
|
|
fi
|
|
|
|
unset GEOINTEL_OPERATOR_PASSWORD
|
|
|
|
PASSWORD_HASH="$(printf '%s\n' "$generated" | sed -n '1p')"
|
|
SESSION_SECRET="$(printf '%s\n' "$generated" | sed -n '2p')"
|
|
|
|
case "$PASSWORD_HASH" in
|
|
pbkdf2_sha256\$*) ;;
|
|
*) echo "Password hash generation failed." >&2; exit 1 ;;
|
|
esac
|
|
if [ "${#SESSION_SECRET}" -lt 32 ]; then
|
|
echo "Session secret generation failed." >&2
|
|
exit 1
|
|
fi
|
|
|
|
backup="${ENV_FILE}.bak.$(date -u +%Y%m%d%H%M%S)"
|
|
cp "$ENV_FILE" "$backup"
|
|
|
|
tmp_env="$(mktemp)"
|
|
grep -v -E '^(GEOINTEL_AUTH_ENABLED|GEOINTEL_AUTH_USERNAME|GEOINTEL_AUTH_PASSWORD_HASH|GEOINTEL_AUTH_SESSION_SECRET|GEOINTEL_GUEST_ACCESS_ENABLED)=' \
|
|
"$ENV_FILE" >"$tmp_env" || true
|
|
|
|
{
|
|
printf '\n'
|
|
printf '# Operator login, written by scripts/configure_operator_login.sh on %s.\n' \
|
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
# Single quotes zijn verplicht: de pbkdf2-hash bevat '$'-tekens. Ongequote
|
|
# zou Docker Compose ze als variabelen interpoleren en `source .env` onder
|
|
# `set -u` afbreken met "unbound variable".
|
|
printf 'GEOINTEL_AUTH_ENABLED=true\n'
|
|
printf "GEOINTEL_AUTH_USERNAME='%s'\n" "$USERNAME"
|
|
printf "GEOINTEL_AUTH_PASSWORD_HASH='%s'\n" "$PASSWORD_HASH"
|
|
printf "GEOINTEL_AUTH_SESSION_SECRET='%s'\n" "$SESSION_SECRET"
|
|
printf 'GEOINTEL_GUEST_ACCESS_ENABLED=%s\n' "$GUEST_ACCESS"
|
|
} >>"$tmp_env"
|
|
|
|
cat "$tmp_env" >"$ENV_FILE"
|
|
rm -f "$tmp_env"
|
|
chmod 600 "$ENV_FILE"
|
|
|
|
echo "Operator login configured in ${ENV_FILE}."
|
|
echo " username: ${USERNAME}"
|
|
echo " password hash: pbkdf2_sha256, 600000 iterations"
|
|
echo " session secret: regenerated (existing browser sessions are invalidated)"
|
|
echo " guest access: ${GUEST_ACCESS}"
|
|
echo " backup of the previous env file: ${backup}"
|
|
echo
|
|
echo "Apply with: bash deploy/unraid/deploy-release.sh"
|