#!/usr/bin/env bash set -euo pipefail # init_knoe_users.sh # Purpose: # - Provision Kerberos principals and database accounts for knoe-system users # - Creates: admin@PROLE.LOCAL (master password), guest@PROLE.LOCAL (read-only), # postgres service principal (keytab for GSS auth), developer group role # - Cross-realm trust with myrddin.prole.org PROLE.ORG is activated via # PROLE_KDC_TRUST_REALM=PROLE.ORG in init_kdc.sh / init_knoe_auth.sh # - Sets up service admin access: ArgoCD RBAC, Gitea, GitLab # # Prerequisites: # init_kdc.sh initialize (with PROLE_KDC_REALM=PROLE.LOCAL, # PROLE_KDC_TRUST_REALM=PROLE.ORG, # KRB5_KDC=myrddin.prole.org) # init_cnpg_backup.sh (CNPG cluster must exist) # init_argocd.sh (ArgoCD must be running) # # Usage: # ./init_knoe_users.sh initialize # create/update all user resources # ./init_knoe_users.sh status # show current state # ./init_knoe_users.sh cleanup # remove managed secrets/patches SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/knoe_cfg.sh" if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then knoe_set_mode "${2:-}" shift 2 elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then knoe_set_mode "${1#*=}" shift fi knoe_ensure_kubeconfig >/dev/null 2>&1 || true ensure_kube_context || exit 1 # Guard: we must never operate against the 'default' context — it means the # desired context was not found and a placeholder was silently accepted. _active_ctx=$(kubectl config current-context 2>/dev/null || true) if [[ "${_active_ctx:-}" == "default" ]]; then printf '[ERROR] Active kubectl context is '\''default'\'' — the required context was not found.\n' >&2 printf ' Add the correct kubeconfig to KUBECONFIG, e.g.:\n' >&2 printf ' export KUBECONFIG=%s:~/.kube/config\n' "${KUBECONFIG:-/path/to/knoe-k3s.kubeconfig}" >&2 printf ' Then retry.\n' >&2 exit 1 fi unset _active_ctx ACTION="${1:-initialize}" resolve_knoe_mode() { local raw_mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-k3s}}}" if command -v knoe_normalize_mode >/dev/null 2>&1; then knoe_normalize_mode "$raw_mode" return 0 fi raw_mode=$(printf '%s' "$raw_mode" | tr 'A-Z' 'a-z') case "$raw_mode" in prod|production) printf 'k8s' ;; *) printf '%s' "$raw_mode" ;; esac } default_knoe_auth_deployment() { case "$(resolve_knoe_mode)" in k8s) printf 'authority-gcp-auth' ;; *) printf 'authority-knoe-auth' ;; esac } # ── Configurable variables ───────────────────────────────────────────────── # PROLE_KDC_NAMESPACE is the canonical env set by init_kdc.sh; honour it here # so both scripts look at the same namespace for knoe-kdc-secrets. # Never fall through to "default" — use knoe-system as the last resort. KNOE_USERS_NAMESPACE="${KNOE_USERS_NAMESPACE:-${SERVICE_NAMESPACE:-knoe-system}}" KNOE_KDC_NAMESPACE="${KNOE_KDC_NAMESPACE:-${PROLE_KDC_NAMESPACE:-${KNOE_USERS_NAMESPACE}}}" KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}" KNOE_DB_CLUSTER="${KNOE_DB_CLUSTER:-knoe-db}" KNOE_DEPLOYMENT_MODE="${KNOE_DEPLOYMENT_MODE:-$(resolve_knoe_mode)}" KNOE_AUTH_DEPLOYMENT="${KNOE_AUTH_DEPLOYMENT:-${PROLE_KDC_NAME:-$(default_knoe_auth_deployment)}}" KNOE_ADMIN_PRINCIPAL="${KNOE_ADMIN_PRINCIPAL:-admin}" PROLE_KDC_REALM="${PROLE_KDC_REALM:-PROLE.LOCAL}" GITEA_HOST="${GITEA_HOST:-git.prole.org}" GITLAB_HOST="${GITLAB_HOST:-gitlab.prole.org}" GITEA_NAMESPACE="${GITEA_NAMESPACE:-gitea}" GITLAB_NAMESPACE="${GITLAB_NAMESPACE:-gitlab}" ARGOCD_NAMESPACE="${ARGOCD_NAMESPACE:-argocd}" # Provided externally or resolved from knoe-kdc-secrets PROLE_KDC_MASTER_PASSWORD="${PROLE_KDC_MASTER_PASSWORD:-}" KNOE_GUEST_PASSWORD="${KNOE_GUEST_PASSWORD:-}" # UI-supplied credentials (set by knoe_users.py from the Database / Kerberos Auth screens) PROLE_LOCAL_ADMIN_PASSWORD="${PROLE_LOCAL_ADMIN_PASSWORD:-}" PROLE_ORG_ADMIN_PASSWORD="${PROLE_ORG_ADMIN_PASSWORD:-}" # Optional: personal access tokens for service admin promotion GITEA_ADMIN_TOKEN="${GITEA_ADMIN_TOKEN:-}" GITLAB_ADMIN_TOKEN="${GITLAB_ADMIN_TOKEN:-}" log() { printf '[INFO] %s\n' "$*"; } err() { printf '[ERROR] %s\n' "$*" >&2; } warn() { printf '[WARN] %s\n' "$*" >&2; } die() { err "$*"; exit 1; } ensure_tools() { for t in kubectl curl python3; do command -v "$t" >/dev/null || die "Missing required tool: $t" done } b64_decode() { if base64 --decode /dev/null 2>&1; then base64 --decode elif base64 -d /dev/null 2>&1; then base64 -d else base64 -D fi } gen_password() { if command -v openssl >/dev/null 2>&1; then openssl rand -base64 18 return 0 fi python3 - <<'PY' import secrets, string alphabet = string.ascii_letters + string.digits print(''.join(secrets.choice(alphabet) for _ in range(24))) PY } # ── Secret helpers ───────────────────────────────────────────────────────── # Read a base64-encoded key from a k8s Secret in KNOE_KDC_NAMESPACE. # Mirrors get_secret_value() in init_kdc.sh (which reads from KDC_NAMESPACE). get_secret_value() { local secret="$1" key="$2" kubectl -n "$KNOE_KDC_NAMESPACE" get secret "$secret" \ -o "jsonpath={.data.${key}}" 2>/dev/null | b64_decode 2>/dev/null || true } # ── KDC helpers ──────────────────────────────────────────────────────────── get_kdc_pod() { # The kdc container lives inside the authority deployment pod selected by mode. # Only return Running pods — Terminating pods from a prior rollout must be excluded. local pod pod=$(kubectl -n "$KNOE_KDC_NAMESPACE" get pods \ -l "app=${KNOE_AUTH_DEPLOYMENT}" \ --field-selector=status.phase=Running \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -n "$pod" ]]; then printf '%s' "$pod" return 0 fi return 0 } ensure_kdc_pod() { local kdc_pod init_kdc_script kdc_pod=$(get_kdc_pod) if [[ -n "$kdc_pod" ]]; then printf '%s' "$kdc_pod" return 0 fi init_kdc_script="$SCRIPT_DIR/init_kdc.sh" [[ -f "$init_kdc_script" ]] || die "Required script not found: $init_kdc_script" log "No authority pod found in namespace ${KNOE_KDC_NAMESPACE}; bootstrapping ${KNOE_AUTH_DEPLOYMENT} via init_kdc.sh ..." >&2 PROLE_KDC_NAMESPACE="$KNOE_KDC_NAMESPACE" \ PROLE_KDC_MASTER_PASSWORD="$PROLE_KDC_MASTER_PASSWORD" \ PROLE_KDC_NAME="$KNOE_AUTH_DEPLOYMENT" \ bash "$init_kdc_script" initialize >&2 kdc_pod=$(get_kdc_pod) [[ -n "$kdc_pod" ]] || die "No KDC pod found in namespace ${KNOE_KDC_NAMESPACE} (label app=${KNOE_AUTH_DEPLOYMENT})." printf '%s' "$kdc_pod" } kdc_principal_exists() { local pod="$1" principal="$2" kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \ kadmin.local -q "get_principal ${principal}" >/dev/null 2>&1 } kdc_addprinc() { local pod="$1" principal="$2" password="$3" kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \ kadmin.local -q "addprinc -pw ${password} ${principal}" } kdc_addprinc_randkey() { local pod="$1" principal="$2" kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \ kadmin.local -q "addprinc -randkey ${principal}" } # ── CNPG helpers ─────────────────────────────────────────────────────────── get_cnpg_primary() { local pod="" # CNPG 1.20+: instanceRole label pod=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \ -l "cnpg.io/cluster=${KNOE_DB_CLUSTER},cnpg.io/instanceRole=primary" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -z "$pod" ]]; then # Older label set pod=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \ -l "cnpg.io/cluster=${KNOE_DB_CLUSTER},role=primary" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) fi printf '%s' "$pod" } cnpg_psql() { local pod="$1"; shift kubectl -n "$KNOE_DB_NAMESPACE" exec "$pod" -- \ psql -U postgres -d postgres "$@" } # ── Main: initialize ─────────────────────────────────────────────────────── initialize() { ensure_tools log "=== knoe-system user provisioning ===" log "Namespaces: kdc/authority=${KNOE_KDC_NAMESPACE} db=${KNOE_DB_NAMESPACE}" log "Mode: ${KNOE_DEPLOYMENT_MODE} authority deployment: ${KNOE_AUTH_DEPLOYMENT}" # 1. Resolve passwords — read from knoe-kdc-secrets (written by init_kdc.sh) if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then PROLE_KDC_MASTER_PASSWORD=$(get_secret_value "knoe-kdc-secrets" "master_password") fi # Allow the UI-supplied PROLE_LOCAL_ADMIN_PASSWORD (DB master password field) to # override when the secret is absent or empty. if [[ -z "$PROLE_KDC_MASTER_PASSWORD" && -n "${PROLE_LOCAL_ADMIN_PASSWORD:-}" ]]; then PROLE_KDC_MASTER_PASSWORD="$PROLE_LOCAL_ADMIN_PASSWORD" log "Using PROLE_LOCAL_ADMIN_PASSWORD as PROLE_KDC_MASTER_PASSWORD" fi if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then die "PROLE_KDC_MASTER_PASSWORD not found. The KDC master password must match the running KDC database and cannot be auto-generated. Resolve one of: 1. Run: ./etc/init_kdc.sh initialize (creates/stores the secret in namespace '${KNOE_KDC_NAMESPACE}') 2. Export: export PROLE_KDC_MASTER_PASSWORD= 3. Set the database master password on the Database screen and retry 4. Check: kubectl -n ${KNOE_KDC_NAMESPACE} get secret knoe-kdc-secrets -o yaml" fi if [[ -z "$KNOE_GUEST_PASSWORD" ]]; then KNOE_GUEST_PASSWORD=$(get_secret_value "knoe-kdc-secrets" "guest_password") fi if [[ -z "$KNOE_GUEST_PASSWORD" ]]; then KNOE_GUEST_PASSWORD=$(gen_password) log "Auto-generated guest password — storing in knoe-kdc-secrets ..." if kubectl -n "$KNOE_KDC_NAMESPACE" get secret knoe-kdc-secrets >/dev/null 2>&1; then local guest_b64 guest_b64=$(printf '%s' "$KNOE_GUEST_PASSWORD" | base64 | tr -d '\n') kubectl -n "$KNOE_KDC_NAMESPACE" patch secret knoe-kdc-secrets \ --type=merge \ --patch "{\"data\":{\"guest_password\":\"${guest_b64}\"}}" >/dev/null else kubectl -n "$KNOE_KDC_NAMESPACE" create secret generic knoe-kdc-secrets \ --from-literal=guest_password="$KNOE_GUEST_PASSWORD" \ --dry-run=client -o yaml | kubectl apply -f - >/dev/null fi fi # 2. Locate (or bootstrap) KDC pod local kdc_pod kdc_pod=$(ensure_kdc_pod) log "KDC pod: $kdc_pod" # 2a. Pre-flight: verify the KDC database stash file is present and kadmin.local # can access the database. A missing stash means init_kdc.sh has not been # run (or the pod restarted with ephemeral storage — EmptyDir volumes are # wiped on pod restart). Auto-recover by re-initialising the database. log "Verifying KDC database accessibility ..." if ! kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ sh -c 'kadmin.local -q listprincs' >/dev/null 2>&1; then log "KDC database not accessible — pod may have restarted with ephemeral storage." log "Re-initialising KDC database via init_kdc.sh ..." local init_kdc_script="$SCRIPT_DIR/init_kdc.sh" [[ -f "$init_kdc_script" ]] || die "Required script not found: $init_kdc_script" PROLE_KDC_NAMESPACE="$KNOE_KDC_NAMESPACE" \ PROLE_KDC_MASTER_PASSWORD="$PROLE_KDC_MASTER_PASSWORD" \ PROLE_KDC_NAME="$KNOE_AUTH_DEPLOYMENT" \ bash "$init_kdc_script" initialize >&2 # Poll until the new pod's entrypoint finishes kdb5_util create. # The rollout completes before the in-container DB init finishes (no readiness probe). local _reinit_timeout=120 _reinit_interval=5 _reinit_elapsed=0 log "Waiting up to ${_reinit_timeout}s for KDC database to become accessible ..." while (( _reinit_elapsed < _reinit_timeout )); do kdc_pod=$(get_kdc_pod) if [[ -n "$kdc_pod" ]] && kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ sh -c 'kadmin.local -q listprincs' >/dev/null 2>&1; then log "KDC database re-initialised successfully (pod: $kdc_pod)" break fi sleep "$_reinit_interval" _reinit_elapsed=$(( _reinit_elapsed + _reinit_interval )) done if (( _reinit_elapsed >= _reinit_timeout )); then die "KDC database still not accessible after ${_reinit_timeout}s (pod: ${kdc_pod:-}). Check init_kdc.sh logs above for errors." fi else log "KDC database accessible" fi # 3. Create admin@PROLE.LOCAL (UI login with master password) local admin_princ="${KNOE_ADMIN_PRINCIPAL}@${PROLE_KDC_REALM}" if kdc_principal_exists "$kdc_pod" "$admin_princ"; then log "${admin_princ} already exists" else log "Creating ${admin_princ} ..." kdc_addprinc "$kdc_pod" "$admin_princ" "$PROLE_KDC_MASTER_PASSWORD" fi # 4. Create guest@PROLE.LOCAL (read-only) local guest_princ="guest@${PROLE_KDC_REALM}" if kdc_principal_exists "$kdc_pod" "$guest_princ"; then log "${guest_princ} already exists" else log "Creating ${guest_princ} ..." kdc_addprinc "$kdc_pod" "$guest_princ" "$KNOE_GUEST_PASSWORD" fi # 5. Ensure BOTH cross-realm trust principals exist in the MIT KDC # Direction A: krbtgt/PROLE.ORG@PROLE.LOCAL — cluster users → AD services # (created by init_kdc.sh PROLE_KDC_TRUST_REALM mechanism) # Direction B: krbtgt/PROLE.LOCAL@PROLE.ORG — AD users → cluster services ← THIS IS WHAT WE NEED local trust_princ_in="krbtgt/PROLE.ORG@${PROLE_KDC_REALM}" local trust_princ_out="krbtgt/${PROLE_KDC_REALM}@PROLE.ORG" local trust_shared_pw trust_shared_pw=$(kubectl -n "$KNOE_KDC_NAMESPACE" get secret knoe-kdc-secrets \ -o jsonpath='{.data.trust_shared_password}' 2>/dev/null | b64_decode || true) if [[ -z "$trust_shared_pw" ]]; then warn "trust_shared_password not set in knoe-kdc-secrets; cross-realm trust principals will not be created" warn "Add trust_shared_password to knoe-kdc-secrets and re-run" else # Direction A (init_kdc.sh may already have created this) if kdc_principal_exists "$kdc_pod" "$trust_princ_in"; then log "${trust_princ_in} already exists" else log "Creating cross-realm principal ${trust_princ_in} ..." kdc_addprinc "$kdc_pod" "$trust_princ_in" "$trust_shared_pw" fi # Direction B — the principal that allows AD users to reach cluster services if kdc_principal_exists "$kdc_pod" "$trust_princ_out"; then log "${trust_princ_out} already exists" else log "Creating cross-realm principal ${trust_princ_out} ..." kdc_addprinc "$kdc_pod" "$trust_princ_out" "$trust_shared_pw" fi fi # 6. Create postgres service principal + export keytab. # Always (re)create the SPN with a fresh random key — the old key is gone when # the KDC pod restarts on ephemeral storage. kadmin.local -q exits 0 even on # failure, so we verify success by checking the keytab file was written. local pg_spn="postgres/knoe-db-rw.${KNOE_DB_NAMESPACE}.svc.cluster.local@${PROLE_KDC_REALM}" log "Creating/refreshing service principal ${pg_spn} ..." kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ sh -c "kadmin.local -q 'delprinc -force ${pg_spn}' 2>/dev/null; kadmin.local -q 'addprinc -randkey ${pg_spn}'" log "Exporting keytab for ${pg_spn} ..." kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ sh -c "rm -f /tmp/pg.keytab; kadmin.local -q 'ktadd -k /tmp/pg.keytab ${pg_spn}'" # kadmin.local -q exits 0 even on failure — verify the file was actually written kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ sh -c '[ -s /tmp/pg.keytab ]' \ || die "Keytab export failed for ${pg_spn} — /tmp/pg.keytab is missing or empty. Check KDC logs." log "Storing keytab in Secret knoe-db-pg-keytab ..." local keytab_b64 # Use pipefail so a base64 failure propagates through the tr pipeline keytab_b64=$(kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ sh -c 'set -o pipefail; base64 /tmp/pg.keytab | tr -d "\n"') [[ -n "$keytab_b64" ]] || die "base64 encoding of keytab produced empty output." # Write secret into the DB namespace where CNPG mounts it kubectl -n "$KNOE_DB_NAMESPACE" create secret generic knoe-db-pg-keytab \ --from-literal=pg.keytab="$(printf '%s' "$keytab_b64" | b64_decode)" \ --dry-run=client -o yaml | kubectl apply -f - >/dev/null log "knoe-db-pg-keytab stored in namespace ${KNOE_DB_NAMESPACE}" # 6. Patch CNPG cluster with managed.roles (idempotent merge) log "Patching CNPG cluster ${KNOE_DB_CLUSTER} managed.roles ..." kubectl -n "$KNOE_DB_NAMESPACE" patch cluster "$KNOE_DB_CLUSTER" \ --type=merge --patch '{ "spec": { "managed": { "roles": [ { "name": "admin", "ensure": "present", "login": true, "superuser": true, "comment": "Kerberos admin principal — full cluster access" }, { "name": "guest", "ensure": "present", "login": true, "superuser": false, "comment": "Kerberos guest principal — read-only access to demo schema" }, { "name": "developer", "ensure": "present", "login": false, "superuser": false, "comment": "Developer group role — granted to knoe-system user accounts" } ] } } }' # 7. Locate CNPG primary and run SQL local primary primary=$(get_cnpg_primary) if [[ -z "$primary" ]]; then warn "CNPG primary pod not found — skipping SQL setup (run again once cluster is ready)" else log "CNPG primary pod: $primary" log "Setting up demo schema and role grants ..." cnpg_psql "$primary" <<'SQL' -- demo schema for guest read-only access (evolves over time) CREATE SCHEMA IF NOT EXISTS demo; -- Ensure roles exist (CNPG managed.roles may not have reconciled yet) DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'admin') THEN CREATE ROLE admin LOGIN SUPERUSER; ELSE ALTER ROLE admin SUPERUSER LOGIN; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'guest') THEN CREATE ROLE guest LOGIN; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'developer') THEN CREATE ROLE developer NOLOGIN; END IF; END $$; -- guest: read-only on demo schema GRANT USAGE ON SCHEMA demo TO guest; GRANT SELECT ON ALL TABLES IN SCHEMA demo TO guest; ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT ON TABLES TO guest; -- developer: read/write access on public and demo schemas GRANT USAGE ON SCHEMA public TO developer; GRANT USAGE ON SCHEMA demo TO developer; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO developer; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA demo TO developer; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO developer; ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO developer; SQL log "SQL setup complete" fi # 8. Create knoe.user schema tables and bootstrap users if [[ -n "$primary" ]]; then create_knoe_schema "$primary" provision_user "$primary" "chrisfu" "chrisfu@prole.org" "Chris Fu" "admin" provision_user "$primary" "ron" "ron@prole.org" "Ron" "developer" else warn "CNPG primary not found — skipping knoe.user schema and user provisioning" fi # 9. ArgoCD RBAC patch patch_argocd_rbac # 10. Gitea admin promotion promote_gitea_admin # 11. GitLab admin promotion promote_gitlab_admin log "=== Initialization complete ===" log "" log "Next steps:" if [[ "$KNOE_DEPLOYMENT_MODE" != "k3d" ]]; then log " 1. On myrddin.prole.org: add krbtgt/PROLE.LOCAL@PROLE.ORG trust principal" log " (samba-tool domain trust, using trust_shared_password from knoe-kdc-secrets)" log " 2. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize" else log " 1. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize" fi } # ── knoe.user schema + user provisioning ────────────────────────────────── create_knoe_schema() { local primary="$1" log "Creating knoe.user schema tables (idempotent)..." cnpg_psql "$primary" <<'SQL' CREATE TABLE IF NOT EXISTS knoe.user ( id SERIAL PRIMARY KEY, username TEXT NOT NULL UNIQUE, realm TEXT NOT NULL DEFAULT 'PROLE.LOCAL', email TEXT, display_name TEXT, tenant_realm TEXT, is_realm_admin BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS knoe.user_role ( user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE, role TEXT NOT NULL, granted_at TIMESTAMPTZ DEFAULT now(), PRIMARY KEY (user_id, role) ); GRANT SELECT, INSERT, UPDATE ON knoe.user TO knoe; GRANT SELECT, INSERT, UPDATE ON knoe.user_role TO knoe; GRANT USAGE, SELECT ON SEQUENCE knoe.user_id_seq TO knoe; SQL log "knoe.user schema ready." } # provision_user # role: admin | developer | guest (default: developer) # Idempotent — safe to re-run. provision_user() { local primary="$1" username="$2" email="${3:-}" display_name="${4:-$2}" role="${5:-developer}" log "Provisioning user: ${username} <${email}> role=${role}" # 1. Kerberos principal (randkey — user sets own password via kpasswd / kadmin) local user_princ="${username}@${PROLE_KDC_REALM}" local kdc_pod kdc_pod=$(get_kdc_pod) if [[ -n "$kdc_pod" ]]; then if kdc_principal_exists "$kdc_pod" "$user_princ"; then log " Kerberos: ${user_princ} already exists" else log " Kerberos: creating ${user_princ} ..." kdc_addprinc_randkey "$kdc_pod" "$user_princ" fi else warn " No KDC pod — skipping Kerberos principal for ${username}" fi # 2. PostgreSQL role (GSS-API: username@PROLE.LOCAL → pg role 'username') cnpg_psql "$primary" -v username="$username" -v role="$role" </dev/null 2>&1; then warn "argocd-rbac-cm not found in namespace ${ARGOCD_NAMESPACE}; skipping ArgoCD RBAC patch" return 0 fi local admin_line="g, ${KNOE_ADMIN_PRINCIPAL}, role:admin" local current_csv current_csv=$(kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm \ -o jsonpath='{.data.policy\.csv}' 2>/dev/null || true) if printf '%s' "$current_csv" | grep -qF "$admin_line"; then log "ArgoCD RBAC already contains admin entry" return 0 fi local new_csv="${current_csv}"$'\n'"${admin_line}" local escaped_csv escaped_csv=$(python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' <<< "$new_csv") kubectl -n "$ARGOCD_NAMESPACE" patch configmap argocd-rbac-cm \ --type=merge \ --patch "{\"data\":{\"policy.csv\":${escaped_csv}}}" >/dev/null log "ArgoCD RBAC: added '${admin_line}'" } # ── Gitea admin promotion ─────────────────────────────────────────────────── promote_gitea_admin() { local token="$GITEA_ADMIN_TOKEN" if [[ -z "$token" ]]; then token=$(kubectl -n "$GITEA_NAMESPACE" get secret gitea-admin-token \ -o jsonpath='{.data.token}' 2>/dev/null | b64_decode || true) fi # Auto-generate an admin token by exec-ing into the Gitea pod (Gitea ≥ 1.17) if [[ -z "$token" ]]; then local _gitea_pod _gitea_pod=$(kubectl -n "$GITEA_NAMESPACE" get pods \ -l "app.kubernetes.io/name=gitea" \ --field-selector=status.phase=Running \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -n "$_gitea_pod" ]]; then log "Gitea: generating admin token via kubectl exec into ${_gitea_pod} ..." token=$(kubectl -n "$GITEA_NAMESPACE" exec "$_gitea_pod" -- \ gitea admin user generate-access-token \ --username admin --token-name knoe-installer \ --scopes "write:admin,read:user" --raw 2>/dev/null | tail -1 || true) if [[ -n "$token" ]]; then # Persist so subsequent calls (and future re-runs) reuse the same token local _tok_b64; _tok_b64=$(printf '%s' "$token" | base64 | tr -d '\n') kubectl -n "$GITEA_NAMESPACE" create secret generic gitea-admin-token \ --from-literal=token="$token" \ --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true log "Gitea: admin token generated and stored in gitea-admin-token secret" fi fi fi if [[ -z "$token" ]]; then warn "No GITEA_ADMIN_TOKEN available — skipping Gitea admin promotion" warn " Manually: curl -X PATCH https://${GITEA_HOST}/api/v1/admin/users/${KNOE_ADMIN_PRINCIPAL} \\" warn " -H 'Authorization: token ' -H 'Content-Type: application/json' \\" warn " -d '{\"admin\":true,\"login_name\":\"${KNOE_ADMIN_PRINCIPAL}\",\"source_id\":0}'" return 0 fi local http_code http_code=$(curl -s -o /dev/null -w '%{http_code}' \ -H "Authorization: token ${token}" \ "https://${GITEA_HOST}/api/v1/users/${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true) if [[ "$http_code" == "404" ]]; then warn "Gitea: user '${KNOE_ADMIN_PRINCIPAL}' not found (must log in first)" warn " Re-run '$0 initialize' after first login" return 0 fi http_code=$(curl -s -o /dev/null -w '%{http_code}' \ -X PATCH \ -H "Authorization: token ${token}" \ -H "Content-Type: application/json" \ -d "{\"admin\":true,\"login_name\":\"${KNOE_ADMIN_PRINCIPAL}\",\"source_id\":0}" \ "https://${GITEA_HOST}/api/v1/admin/users/${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true) if [[ "$http_code" =~ ^2 ]]; then log "Gitea: '${KNOE_ADMIN_PRINCIPAL}' promoted to admin" else warn "Gitea admin promotion returned HTTP ${http_code}" fi } # ── GitLab admin promotion ───────────────────────────────────────────────── promote_gitlab_admin() { local token="$GITLAB_ADMIN_TOKEN" if [[ -z "$token" ]]; then warn "No GITLAB_ADMIN_TOKEN (personal access token) available — skipping GitLab admin promotion" warn " Set GITLAB_ADMIN_TOKEN and re-run, or use:" warn " kubectl exec -n ${GITLAB_NAMESPACE} -- gitlab-rails runner \\" warn " \"User.find_by_username('${KNOE_ADMIN_PRINCIPAL}')&.update(admin: true)\"" return 0 fi # Look up user ID by username local user_json user_id user_json=$(curl -s \ -H "PRIVATE-TOKEN: ${token}" \ "https://${GITLAB_HOST}/api/v4/users?username=${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true) user_id=$(python3 -c \ 'import sys,json; u=json.load(sys.stdin); print(u[0]["id"] if u else "")' \ <<< "$user_json" 2>/dev/null || true) if [[ -z "$user_id" ]]; then warn "GitLab: user '${KNOE_ADMIN_PRINCIPAL}' not found (must log in first)" warn " Re-run '$0 initialize' after first login" return 0 fi local http_code http_code=$(curl -s -o /dev/null -w '%{http_code}' \ -X PUT \ -H "PRIVATE-TOKEN: ${token}" \ -H "Content-Type: application/json" \ -d '{"admin":true}' \ "https://${GITLAB_HOST}/api/v4/users/${user_id}" 2>/dev/null || true) if [[ "$http_code" =~ ^2 ]]; then log "GitLab: '${KNOE_ADMIN_PRINCIPAL}' promoted to admin" else warn "GitLab admin promotion returned HTTP ${http_code}" fi } # ── Status ───────────────────────────────────────────────────────────────── show_status() { log "=== KDC Principals ===" local kdc_pod kdc_pod=$(get_kdc_pod) if [[ -n "$kdc_pod" ]]; then kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \ kadmin.local -q "list_principals" 2>/dev/null \ | grep -E "^(admin|guest|developer|postgres/|krbtgt/)" || true else warn "No KDC pod found" fi log "" log "=== CNPG Managed Roles ===" kubectl -n "$KNOE_DB_NAMESPACE" get cluster "$KNOE_DB_CLUSTER" \ -o jsonpath='{.spec.managed.roles}' 2>/dev/null \ | python3 -m json.tool 2>/dev/null || warn "Could not read managed.roles" log "" log "=== PostgreSQL Roles ===" local primary primary=$(get_cnpg_primary) if [[ -n "$primary" ]]; then cnpg_psql "$primary" -c "\\du admin guest developer" 2>/dev/null || true else warn "No CNPG primary pod found" fi log "" log "=== Keytab Secret ===" if kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-pg-keytab >/dev/null 2>&1; then log "knoe-db-pg-keytab exists in namespace ${KNOE_DB_NAMESPACE}" else warn "knoe-db-pg-keytab NOT FOUND in namespace ${KNOE_DB_NAMESPACE}" fi log "" log "=== ArgoCD RBAC ===" kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm \ -o jsonpath='{.data.policy\.csv}' 2>/dev/null || warn "argocd-rbac-cm not found" } # ── Cleanup ──────────────────────────────────────────────────────────────── cleanup() { warn "cleanup removes the pg keytab secret and clears managed.roles from CNPG." warn "KDC principals are NOT deleted (use kadmin.local to remove them manually)." kubectl -n "$KNOE_DB_NAMESPACE" delete secret knoe-db-pg-keytab \ --ignore-not-found >/dev/null && log "Deleted knoe-db-pg-keytab" kubectl -n "$KNOE_DB_NAMESPACE" patch cluster "$KNOE_DB_CLUSTER" \ --type=merge --patch '{"spec":{"managed":null}}' >/dev/null 2>&1 \ && log "Cleared managed.roles from cluster ${KNOE_DB_CLUSTER}" || true } # ── Dispatch ─────────────────────────────────────────────────────────────── case "$ACTION" in initialize|init) initialize ;; status) show_status ;; cleanup) cleanup ;; *) err "Unknown action: ${ACTION}" err "Usage: $(basename "$0") [initialize|status|cleanup]" exit 1 ;; esac