391 lines
13 KiB
Bash
391 lines
13 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
CONFIG_DIR="${PROJECT_ROOT}/config"
|
|
KEY_DIR="${CONFIG_DIR}/keys"
|
|
CLIENT_ROOT="${CONFIG_DIR}/clients"
|
|
|
|
CLIENT_ID=""
|
|
INSTALLATION_ID=""
|
|
CLOUD_BASE_URL=""
|
|
TOKEN_URL=""
|
|
CLIENT_DIR=""
|
|
CLIENT_PRIVATE_KEY=""
|
|
CLIENT_PUBLIC_KEY=""
|
|
MACHINE_ASSERTION=""
|
|
CLIENT_NAME=""
|
|
DB_HOST_VALUE=""
|
|
DB_PORT_VALUE=""
|
|
DB_NAME_VALUE=""
|
|
DB_USER_VALUE=""
|
|
DB_PASSWORD_VALUE=""
|
|
LICENSE_TYPE=""
|
|
PACKAGE_CODE=""
|
|
LICENSE_MONTHS=""
|
|
PSQL_BIN=""
|
|
SKIP_BUILD_CONFIGURED="${CYGNUS_SETUP_SKIP_BUILD+x}"
|
|
SKIP_BUILD="${CYGNUS_SETUP_SKIP_BUILD:-false}"
|
|
FORCE_ASSERTION="${CYGNUS_SETUP_FORCE_ASSERTION:-false}"
|
|
NON_INTERACTIVE="${CYGNUS_SETUP_NON_INTERACTIVE:-false}"
|
|
|
|
log() {
|
|
printf '\n==> %s\n' "$1"
|
|
}
|
|
|
|
fail() {
|
|
printf '\nERROR: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_command() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' was not found."
|
|
}
|
|
|
|
prompt_value() {
|
|
local prompt="$1"
|
|
local default_value="$2"
|
|
local value=""
|
|
|
|
if [[ "${NON_INTERACTIVE}" == "true" ]] || [[ ! -t 0 ]]; then
|
|
printf '%s' "${default_value}"
|
|
return
|
|
fi
|
|
|
|
read -r -p "${prompt} [${default_value}]: " value
|
|
printf '%s' "${value:-${default_value}}"
|
|
}
|
|
|
|
prompt_yes_no() {
|
|
local prompt="$1"
|
|
local default_value="$2"
|
|
local answer=""
|
|
local hint="y/N"
|
|
|
|
[[ "${default_value}" == "true" ]] && hint="Y/n"
|
|
if [[ "${NON_INTERACTIVE}" == "true" ]] || [[ ! -t 0 ]]; then
|
|
printf '%s' "${default_value}"
|
|
return
|
|
fi
|
|
|
|
read -r -p "${prompt} [${hint}]: " answer
|
|
case "${answer}" in
|
|
y|Y|yes|YES) printf '%s' "true" ;;
|
|
n|N|no|NO) printf '%s' "false" ;;
|
|
"") printf '%s' "${default_value}" ;;
|
|
*) fail "Please answer yes or no." ;;
|
|
esac
|
|
}
|
|
|
|
validate_identifier() {
|
|
local label="$1"
|
|
local value="$2"
|
|
[[ "${value}" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] || fail \
|
|
"${label} must contain no spaces and use only letters, numbers, '-' or '_'."
|
|
}
|
|
|
|
validate_client_slug() {
|
|
[[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || fail \
|
|
"Customer identifier must use lowercase letters, numbers and single hyphens only."
|
|
}
|
|
|
|
collect_client_inputs() {
|
|
local default_client="${CYGNUS_SETUP_CLIENT_ID:-customer-a}"
|
|
local default_installation="${CYGNUS_SETUP_INSTALLATION_ID:-site-01}"
|
|
local default_url="${CYGNUS_SETUP_CLOUD_URL:-http://localhost:8090}"
|
|
|
|
log "Client provisioning details"
|
|
CLIENT_ID="$(prompt_value \
|
|
"Customer identifier (no spaces; used in key and directory names)" \
|
|
"${default_client}")"
|
|
validate_client_slug "${CLIENT_ID}"
|
|
CLIENT_NAME="$(prompt_value \
|
|
"Customer display name" \
|
|
"${CYGNUS_SETUP_CLIENT_NAME:-${CLIENT_ID}}")"
|
|
[[ -n "${CLIENT_NAME}" ]] || fail "Customer display name is required."
|
|
|
|
INSTALLATION_ID="$(prompt_value \
|
|
"Installation identifier (no spaces)" \
|
|
"${default_installation}")"
|
|
validate_identifier "Installation identifier" "${INSTALLATION_ID}"
|
|
|
|
CLOUD_BASE_URL="$(prompt_value "Cloud service base URL" "${default_url}")"
|
|
[[ "${CLOUD_BASE_URL}" =~ ^https?://[^[:space:]]+$ ]] || fail \
|
|
"Cloud service base URL must be a valid http:// or https:// URL."
|
|
|
|
if [[ -z "${SKIP_BUILD_CONFIGURED}" ]]; then
|
|
if [[ "$(prompt_yes_no "Run the complete Maven verification" "true")" == "true" ]]; then
|
|
SKIP_BUILD="false"
|
|
else
|
|
SKIP_BUILD="true"
|
|
fi
|
|
fi
|
|
|
|
TOKEN_URL="${CLOUD_BASE_URL%/}/oauth2/token"
|
|
CLIENT_DIR="${CLIENT_ROOT}/${CLIENT_ID}"
|
|
CLIENT_PRIVATE_KEY="${CLIENT_DIR}/${CLIENT_ID}-signing-private.pem"
|
|
CLIENT_PUBLIC_KEY="${CLIENT_DIR}/${CLIENT_ID}-signing-public.pem"
|
|
MACHINE_ASSERTION="${CLIENT_DIR}/${CLIENT_ID}-${INSTALLATION_ID}-assertion.jwt"
|
|
|
|
DB_HOST_VALUE="$(prompt_value \
|
|
"Cloud PostgreSQL host" "${CYGNUS_SETUP_DB_HOST:-${DB_HOST:-localhost}}")"
|
|
DB_PORT_VALUE="$(prompt_value \
|
|
"Cloud PostgreSQL port" "${CYGNUS_SETUP_DB_PORT:-${DB_PORT:-5432}}")"
|
|
DB_NAME_VALUE="$(prompt_value \
|
|
"Cloud PostgreSQL database" "${CYGNUS_SETUP_DB_NAME:-${DB_NAME:-matrix}}")"
|
|
DB_USER_VALUE="$(prompt_value \
|
|
"Cloud PostgreSQL user" "${CYGNUS_SETUP_DB_USER:-${DB_USER:-postgres}}")"
|
|
DB_PASSWORD_VALUE="${CYGNUS_SETUP_DB_PASSWORD:-${DB_PASSWORD:-}}"
|
|
if [[ -z "${DB_PASSWORD_VALUE}" ]] && [[ "${NON_INTERACTIVE}" != "true" ]] && [[ -t 0 ]]; then
|
|
read -r -s -p "Cloud PostgreSQL password: " DB_PASSWORD_VALUE
|
|
printf '\n'
|
|
fi
|
|
[[ -n "${DB_PASSWORD_VALUE}" ]] || fail \
|
|
"Cloud PostgreSQL password is required through the prompt or CYGNUS_SETUP_DB_PASSWORD."
|
|
|
|
LICENSE_TYPE="$(prompt_value \
|
|
"License type" "${CYGNUS_SETUP_LICENSE_TYPE:-ANNUAL}")"
|
|
PACKAGE_CODE="$(prompt_value \
|
|
"License package code" "${CYGNUS_SETUP_PACKAGE_CODE:-FULL}")"
|
|
LICENSE_MONTHS="$(prompt_value \
|
|
"License validity in months" "${CYGNUS_SETUP_LICENSE_MONTHS:-12}")"
|
|
validate_identifier "License type" "${LICENSE_TYPE}"
|
|
validate_identifier "Package code" "${PACKAGE_CODE}"
|
|
[[ "${LICENSE_MONTHS}" =~ ^[1-9][0-9]*$ ]] || fail \
|
|
"License validity must be a positive whole number of months."
|
|
|
|
printf '\nProvisioning summary:\n'
|
|
printf ' Customer: %s\n' "${CLIENT_ID}"
|
|
printf ' Customer name: %s\n' "${CLIENT_NAME}"
|
|
printf ' Installation: %s\n' "${INSTALLATION_ID}"
|
|
printf ' Cloud URL: %s\n' "${CLOUD_BASE_URL}"
|
|
printf ' Token audience: %s\n' "${TOKEN_URL}"
|
|
printf ' Database: %s@%s:%s/%s\n' \
|
|
"${DB_USER_VALUE}" "${DB_HOST_VALUE}" "${DB_PORT_VALUE}" "${DB_NAME_VALUE}"
|
|
printf ' License: %s / %s / %s month(s)\n' \
|
|
"${LICENSE_TYPE}" "${PACKAGE_CODE}" "${LICENSE_MONTHS}"
|
|
printf ' Verify build: %s\n' "$([[ "${SKIP_BUILD}" == "true" ]] && printf no || printf yes)"
|
|
|
|
if [[ "$(prompt_yes_no "Continue with these values" "true")" != "true" ]]; then
|
|
fail "Provisioning cancelled."
|
|
fi
|
|
}
|
|
|
|
generate_private_key() {
|
|
local path="$1"
|
|
if [[ -f "${path}" ]]; then
|
|
printf 'Keeping existing private key: %s\n' "${path}"
|
|
return
|
|
fi
|
|
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out "${path}"
|
|
chmod 600 "${path}"
|
|
}
|
|
|
|
generate_public_key() {
|
|
local private_path="$1"
|
|
local public_path="$2"
|
|
if [[ -f "${public_path}" ]]; then
|
|
printf 'Keeping existing public key: %s\n' "${public_path}"
|
|
return
|
|
fi
|
|
openssl pkey -in "${private_path}" -pubout -out "${public_path}"
|
|
chmod 644 "${public_path}"
|
|
}
|
|
|
|
configure_java_21() {
|
|
local java_major
|
|
|
|
if [[ "$(uname -s)" == "Darwin" ]] && [[ -x /usr/libexec/java_home ]]; then
|
|
if java_home_21="$(/usr/libexec/java_home -v 21 2>/dev/null)"; then
|
|
export JAVA_HOME="${java_home_21}"
|
|
export PATH="${JAVA_HOME}/bin:${PATH}"
|
|
fi
|
|
fi
|
|
|
|
require_command java
|
|
java_major="$(java -version 2>&1 | awk -F '[".]' '/version/ {print $2; exit}')"
|
|
[[ "${java_major}" == "21" ]] || fail \
|
|
"JDK 21 is required. Current java is: $(java -version 2>&1 | head -n 1)"
|
|
}
|
|
|
|
register_client_in_database() {
|
|
local client_id_b64 client_name_b64 installation_b64 public_key_b64
|
|
local license_type_b64 package_code_b64
|
|
|
|
client_id_b64="$(printf '%s' "${CLIENT_ID}" | openssl base64 -A)"
|
|
client_name_b64="$(printf '%s' "${CLIENT_NAME}" | openssl base64 -A)"
|
|
installation_b64="$(printf '%s' "${INSTALLATION_ID}" | openssl base64 -A)"
|
|
public_key_b64="$(openssl base64 -A -in "${CLIENT_PUBLIC_KEY}")"
|
|
license_type_b64="$(printf '%s' "${LICENSE_TYPE}" | openssl base64 -A)"
|
|
package_code_b64="$(printf '%s' "${PACKAGE_CODE}" | openssl base64 -A)"
|
|
|
|
PGPASSWORD="${DB_PASSWORD_VALUE}" "${PSQL_BIN}" \
|
|
-h "${DB_HOST_VALUE}" \
|
|
-p "${DB_PORT_VALUE}" \
|
|
-U "${DB_USER_VALUE}" \
|
|
-d "${DB_NAME_VALUE}" \
|
|
-X -v ON_ERROR_STOP=1 \
|
|
-c "
|
|
WITH account AS (
|
|
INSERT INTO identity.client_account
|
|
(tenant_id, client_slug, client_name, status)
|
|
VALUES (
|
|
gen_random_uuid(),
|
|
convert_from(decode('${client_id_b64}', 'base64'), 'UTF8'),
|
|
convert_from(decode('${client_name_b64}', 'base64'), 'UTF8'),
|
|
'ACTIVE')
|
|
ON CONFLICT (client_slug) DO UPDATE SET
|
|
client_name = EXCLUDED.client_name,
|
|
status = 'ACTIVE',
|
|
updated_at = now()
|
|
RETURNING tenant_id
|
|
), installation AS (
|
|
INSERT INTO identity.client_installation
|
|
(installation_id, tenant_id, client_id, installation_code,
|
|
assertion_public_key, allowed_scopes, enabled)
|
|
SELECT
|
|
gen_random_uuid(),
|
|
tenant_id,
|
|
convert_from(decode('${client_id_b64}', 'base64'), 'UTF8'),
|
|
convert_from(decode('${installation_b64}', 'base64'), 'UTF8'),
|
|
convert_from(decode('${public_key_b64}', 'base64'), 'UTF8'),
|
|
ARRAY['identity.login']::text[],
|
|
true
|
|
FROM account
|
|
ON CONFLICT (client_id, installation_code) DO UPDATE SET
|
|
tenant_id = EXCLUDED.tenant_id,
|
|
assertion_public_key = EXCLUDED.assertion_public_key,
|
|
allowed_scopes = EXCLUDED.allowed_scopes,
|
|
enabled = true,
|
|
security_version = identity.client_installation.security_version + 1,
|
|
updated_at = now()
|
|
RETURNING tenant_id
|
|
)
|
|
INSERT INTO identity.client_license
|
|
(license_id, tenant_id, license_type, package_code,
|
|
valid_from, valid_until, status)
|
|
SELECT
|
|
gen_random_uuid(),
|
|
tenant_id,
|
|
convert_from(decode('${license_type_b64}', 'base64'), 'UTF8'),
|
|
convert_from(decode('${package_code_b64}', 'base64'), 'UTF8'),
|
|
now(),
|
|
now() + make_interval(months => ${LICENSE_MONTHS}),
|
|
'ACTIVE'
|
|
FROM installation
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM identity.client_license existing
|
|
WHERE existing.tenant_id = installation.tenant_id
|
|
AND existing.status = 'ACTIVE'
|
|
AND existing.valid_until > now()
|
|
);"
|
|
}
|
|
|
|
generate_machine_assertion() {
|
|
if [[ -f "${MACHINE_ASSERTION}" ]] \
|
|
&& [[ "${FORCE_ASSERTION}" != "true" ]]; then
|
|
if [[ "$(prompt_yes_no \
|
|
"Machine assertion already exists. Regenerate it" \
|
|
"false")" == "true" ]]; then
|
|
FORCE_ASSERTION="true"
|
|
else
|
|
printf 'Keeping existing machine assertion: %s\n' "${MACHINE_ASSERTION}"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
[[ -f "${CLIENT_PRIVATE_KEY}" ]] || fail \
|
|
"Customer signing key was not found: ${CLIENT_PRIVATE_KEY}"
|
|
[[ -f "${KEY_DIR}/assertion-decryption-public.pem" ]] || fail \
|
|
"Cloud assertion public key was not found: ${KEY_DIR}/assertion-decryption-public.pem"
|
|
|
|
rm -f "${MACHINE_ASSERTION}"
|
|
mvn -q -pl cygnus-cloud-client exec:java \
|
|
-Dexec.mainClass=com.cygnus.client.provisioning.MachineAssertionGenerator \
|
|
-Dexec.args="${CLIENT_ID} ${INSTALLATION_ID} ${TOKEN_URL} ${CLIENT_PRIVATE_KEY} ${KEY_DIR}/assertion-decryption-public.pem ${MACHINE_ASSERTION}"
|
|
[[ -s "${MACHINE_ASSERTION}" ]] || fail \
|
|
"Machine assertion generation did not create: ${MACHINE_ASSERTION}"
|
|
chmod 600 "${MACHINE_ASSERTION}"
|
|
}
|
|
|
|
main() {
|
|
cd "${PROJECT_ROOT}"
|
|
|
|
collect_client_inputs
|
|
|
|
log "Checking JDK 21, Maven, and OpenSSL"
|
|
require_command awk
|
|
require_command openssl
|
|
require_command mvn
|
|
if command -v psql >/dev/null 2>&1; then
|
|
PSQL_BIN="$(command -v psql)"
|
|
elif [[ -x /Library/PostgreSQL/17/bin/psql ]]; then
|
|
PSQL_BIN="/Library/PostgreSQL/17/bin/psql"
|
|
else
|
|
fail "PostgreSQL psql was not found in PATH or /Library/PostgreSQL/17/bin."
|
|
fi
|
|
configure_java_21
|
|
java -version
|
|
mvn -version
|
|
openssl version
|
|
|
|
if [[ "${SKIP_BUILD}" != "true" ]]; then
|
|
log "Building and verifying all modules"
|
|
mvn clean verify
|
|
else
|
|
log "Skipping Maven verification because CYGNUS_SETUP_SKIP_BUILD=true"
|
|
mvn -q -pl cygnus-cloud-client -am compile
|
|
fi
|
|
|
|
log "Creating protected local configuration directories"
|
|
mkdir -p "${KEY_DIR}" "${CLIENT_DIR}"
|
|
chmod 700 "${CONFIG_DIR}" "${KEY_DIR}" "${CLIENT_ROOT}" "${CLIENT_DIR}"
|
|
|
|
log "Generating cloud assertion-encryption keys"
|
|
generate_private_key "${KEY_DIR}/assertion-decryption-private.pem"
|
|
generate_public_key \
|
|
"${KEY_DIR}/assertion-decryption-private.pem" \
|
|
"${KEY_DIR}/assertion-decryption-public.pem"
|
|
|
|
log "Generating cloud access-token signing keys"
|
|
generate_private_key "${KEY_DIR}/access-token-private.pem"
|
|
generate_public_key \
|
|
"${KEY_DIR}/access-token-private.pem" \
|
|
"${KEY_DIR}/access-token-public.pem"
|
|
|
|
log "Generating cloud login-payload encryption keys"
|
|
generate_private_key "${KEY_DIR}/login-private.pem"
|
|
generate_public_key \
|
|
"${KEY_DIR}/login-private.pem" \
|
|
"${KEY_DIR}/login-public.pem"
|
|
|
|
log "Generating on-premises installation signing keys"
|
|
generate_private_key "${CLIENT_PRIVATE_KEY}"
|
|
generate_public_key \
|
|
"${CLIENT_PRIVATE_KEY}" \
|
|
"${CLIENT_PUBLIC_KEY}"
|
|
|
|
log "Registering tenant, installation, and license in the cloud database"
|
|
register_client_in_database
|
|
|
|
log "Generating encrypted machine assertion"
|
|
generate_machine_assertion
|
|
|
|
log "Local communication security setup completed"
|
|
printf '%s\n' \
|
|
"Client ID: ${CLIENT_ID}" \
|
|
"Installation ID: ${INSTALLATION_ID}" \
|
|
"Token audience: ${TOKEN_URL}" \
|
|
"Registration: PostgreSQL identity schema" \
|
|
"Assertion file: ${MACHINE_ASSERTION}" \
|
|
"Login public key: ${KEY_DIR}/login-public.pem" \
|
|
"" \
|
|
"Next: configure the cloud and on-prem environment variables, then start" \
|
|
"the cloud service on ${CLOUD_BASE_URL} before starting the on-prem app."
|
|
}
|
|
|
|
main "$@"
|