From 21afb58b334a055de50fe4386444523eabf998b8 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 24 Feb 2026 17:46:15 -0600 Subject: [PATCH] feat: multi-instance Authentik credentials with test_user support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add -a flag to all Authentik wrapper scripts, matching the existing multi-instance pattern used by Woodpecker and Cloudflare. credentials.json now supports per-instance Authentik config: authentik..url — instance URL authentik..token — API token (admin wrappers) authentik..test_user — username/password (Playwright/agent tests) authentik.default — default instance name Legacy flat structure (authentik.url) still works as fallback. Token cache is now per-instance (~/.cache/mosaic/authentik-token-). Co-Authored-By: Claude Opus 4.6 --- tools/_lib/credentials.sh | 42 ++++++++++++++++++++++++++------- tools/authentik/admin-status.sh | 24 ++++++++++++------- tools/authentik/app-list.sh | 26 +++++++++++++------- tools/authentik/auth-token.sh | 35 +++++++++++++++++---------- tools/authentik/flow-list.sh | 20 +++++++++++----- tools/authentik/group-list.sh | 26 +++++++++++++------- tools/authentik/user-create.sh | 21 +++++++++++------ tools/authentik/user-list.sh | 28 ++++++++++++++-------- 8 files changed, 152 insertions(+), 70 deletions(-) diff --git a/tools/_lib/credentials.sh b/tools/_lib/credentials.sh index 4e15034..9cc53bb 100755 --- a/tools/_lib/credentials.sh +++ b/tools/_lib/credentials.sh @@ -61,7 +61,8 @@ Usage: load_credentials Services and exported variables: portainer → PORTAINER_URL, PORTAINER_API_KEY coolify → COOLIFY_URL, COOLIFY_TOKEN - authentik → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_USERNAME, AUTHENTIK_PASSWORD + authentik → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_TEST_USER, AUTHENTIK_TEST_PASSWORD (uses default instance) + authentik- → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_TEST_USER, AUTHENTIK_TEST_PASSWORD (specific instance, e.g. authentik-usc) glpi → GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN github → GITHUB_TOKEN gitea-mosaicstack → GITEA_URL, GITEA_TOKEN @@ -91,13 +92,38 @@ EOF [[ -n "$COOLIFY_URL" ]] || { echo "Error: coolify.url not found" >&2; return 1; } [[ -n "$COOLIFY_TOKEN" ]] || { echo "Error: coolify.app_token not found" >&2; return 1; } ;; - authentik) - export AUTHENTIK_URL="${AUTHENTIK_URL:-$(_mosaic_read_cred '.authentik.url')}" - export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}" - export AUTHENTIK_USERNAME="${AUTHENTIK_USERNAME:-$(_mosaic_read_cred '.authentik.username')}" - export AUTHENTIK_PASSWORD="${AUTHENTIK_PASSWORD:-$(_mosaic_read_cred '.authentik.password')}" + authentik-*) + local ak_instance="${service#authentik-}" + export AUTHENTIK_URL="$(_mosaic_read_cred ".authentik.${ak_instance}.url")" + export AUTHENTIK_TOKEN="$(_mosaic_read_cred ".authentik.${ak_instance}.token")" + export AUTHENTIK_TEST_USER="$(_mosaic_read_cred ".authentik.${ak_instance}.test_user.username")" + export AUTHENTIK_TEST_PASSWORD="$(_mosaic_read_cred ".authentik.${ak_instance}.test_user.password")" + export AUTHENTIK_INSTANCE="$ak_instance" AUTHENTIK_URL="${AUTHENTIK_URL%/}" - [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; } + [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.${ak_instance}.url not found" >&2; return 1; } + ;; + authentik) + local ak_default + ak_default="${AUTHENTIK_INSTANCE:-$(_mosaic_read_cred '.authentik.default')}" + if [[ -z "$ak_default" ]]; then + # Fallback: try legacy flat structure (.authentik.url) + local legacy_url + legacy_url="$(_mosaic_read_cred '.authentik.url')" + if [[ -n "$legacy_url" ]]; then + export AUTHENTIK_URL="${AUTHENTIK_URL:-$legacy_url}" + export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}" + export AUTHENTIK_TEST_USER="${AUTHENTIK_TEST_USER:-$(_mosaic_read_cred '.authentik.test_user.username')}" + export AUTHENTIK_TEST_PASSWORD="${AUTHENTIK_TEST_PASSWORD:-$(_mosaic_read_cred '.authentik.test_user.password')}" + AUTHENTIK_URL="${AUTHENTIK_URL%/}" + [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; } + else + echo "Error: authentik.default not set and no AUTHENTIK_INSTANCE env var" >&2 + echo "Available instances: $(jq -r '.authentik | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2 + return 1 + fi + else + load_credentials "authentik-${ak_default}" + fi ;; glpi) export GLPI_URL="${GLPI_URL:-$(_mosaic_read_cred '.glpi.url')}" @@ -177,7 +203,7 @@ EOF ;; *) echo "Error: Unknown service '$service'" >&2 - echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-], cloudflare[-]" >&2 + echo "Supported: portainer, coolify, authentik[-], glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-], cloudflare[-]" >&2 return 1 ;; esac diff --git a/tools/authentik/admin-status.sh b/tools/authentik/admin-status.sh index 92927b0..76eaa09 100755 --- a/tools/authentik/admin-status.sh +++ b/tools/authentik/admin-status.sh @@ -2,29 +2,37 @@ # # admin-status.sh — Authentik system health and version info # -# Usage: admin-status.sh [-f format] +# Usage: admin-status.sh [-f format] [-a instance] # # Options: -# -f format Output format: table (default), json -# -h Show this help +# -f format Output format: table (default), json +# -a instance Authentik instance name (e.g. usc, mosaic) +# -h Show this help set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik FORMAT="table" +AK_INSTANCE="" -while getopts "f:h" opt; do +while getopts "f:a:h" opt; do case $opt in f) FORMAT="$OPTARG" ;; - h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 [-f format]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-a instance]" >&2; exit 1 ;; esac done -TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"}) response=$(curl -sk -w "\n%{http_code}" \ -H "Authorization: Bearer $TOKEN" \ diff --git a/tools/authentik/app-list.sh b/tools/authentik/app-list.sh index 8a6e271..1c26e30 100755 --- a/tools/authentik/app-list.sh +++ b/tools/authentik/app-list.sh @@ -2,32 +2,40 @@ # # app-list.sh — List Authentik applications # -# Usage: app-list.sh [-f format] [-s search] +# Usage: app-list.sh [-f format] [-s search] [-a instance] # # Options: -# -f format Output format: table (default), json -# -s search Search by application name -# -h Show this help +# -f format Output format: table (default), json +# -s search Search by application name +# -a instance Authentik instance name (e.g. usc, mosaic) +# -h Show this help set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik FORMAT="table" SEARCH="" +AK_INSTANCE="" -while getopts "f:s:h" opt; do +while getopts "f:s:a:h" opt; do case $opt in f) FORMAT="$OPTARG" ;; s) SEARCH="$OPTARG" ;; - h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 [-f format] [-s search]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s search] [-a instance]" >&2; exit 1 ;; esac done -TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"}) PARAMS="ordering=name" [[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}" diff --git a/tools/authentik/auth-token.sh b/tools/authentik/auth-token.sh index 5f2c78c..c7d1126 100755 --- a/tools/authentik/auth-token.sh +++ b/tools/authentik/auth-token.sh @@ -2,17 +2,18 @@ # # auth-token.sh — Obtain and cache Authentik API token # -# Usage: auth-token.sh [-f] [-q] +# Usage: auth-token.sh [-f] [-q] [-a instance] # # Returns a valid Authentik API token. Checks in order: -# 1. Cached token at ~/.cache/mosaic/authentik-token (if valid) -# 2. Pre-configured token from credentials.json (authentik.token) +# 1. Cached token at ~/.cache/mosaic/authentik-token- (if valid) +# 2. Pre-configured token from credentials.json (authentik..token) # 3. Fails with instructions to create a token in the admin UI # # Options: -# -f Force re-validation (ignore cached token) -# -q Quiet mode — only output the token -# -h Show this help +# -f Force re-validation (ignore cached token) +# -q Quiet mode — only output the token +# -a instance Authentik instance name (e.g. usc, mosaic) +# -h Show this help # # Environment variables (or credentials.json): # AUTHENTIK_URL — Authentik instance URL @@ -21,22 +22,30 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik -CACHE_DIR="$HOME/.cache/mosaic" -CACHE_FILE="$CACHE_DIR/authentik-token" FORCE=false QUIET=false +AK_INSTANCE="" -while getopts "fqh" opt; do +while getopts "fqa:h" opt; do case $opt in f) FORCE=true ;; q) QUIET=true ;; - h) head -20 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 [-f] [-q]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -22 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f] [-q] [-a instance]" >&2; exit 1 ;; esac done +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + +CACHE_DIR="$HOME/.cache/mosaic" +CACHE_FILE="$CACHE_DIR/authentik-token${AUTHENTIK_INSTANCE:+-$AUTHENTIK_INSTANCE}" + _validate_token() { local token="$1" local http_code @@ -82,5 +91,5 @@ echo " 1. Log into Authentik admin: ${AUTHENTIK_URL}/if/admin/#/core/tokens" >& echo " 2. Click 'Create' → set identifier (e.g., 'mosaic-agent')" >&2 echo " 3. Select 'API Token' intent, uncheck 'Expiring'" >&2 echo " 4. Copy the key and add to credentials.json:" >&2 -echo " jq '.authentik.token = \"\"' credentials.json > tmp && mv tmp credentials.json" >&2 +echo " Add token to credentials.json under authentik..token" >&2 exit 1 diff --git a/tools/authentik/flow-list.sh b/tools/authentik/flow-list.sh index e7bbf93..4ecc0aa 100755 --- a/tools/authentik/flow-list.sh +++ b/tools/authentik/flow-list.sh @@ -2,32 +2,40 @@ # # flow-list.sh — List Authentik flows # -# Usage: flow-list.sh [-f format] [-d designation] +# Usage: flow-list.sh [-f format] [-d designation] [-a instance] # # Options: # -f format Output format: table (default), json # -d designation Filter by designation (authentication, authorization, enrollment, etc.) +# -a instance Authentik instance name (e.g. usc, mosaic) # -h Show this help set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik FORMAT="table" DESIGNATION="" +AK_INSTANCE="" -while getopts "f:d:h" opt; do +while getopts "f:d:a:h" opt; do case $opt in f) FORMAT="$OPTARG" ;; d) DESIGNATION="$OPTARG" ;; - h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 [-f format] [-d designation]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-d designation] [-a instance]" >&2; exit 1 ;; esac done -TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"}) PARAMS="ordering=slug" [[ -n "$DESIGNATION" ]] && PARAMS="${PARAMS}&designation=${DESIGNATION}" diff --git a/tools/authentik/group-list.sh b/tools/authentik/group-list.sh index a3f501d..4db85c0 100755 --- a/tools/authentik/group-list.sh +++ b/tools/authentik/group-list.sh @@ -2,32 +2,40 @@ # # group-list.sh — List Authentik groups # -# Usage: group-list.sh [-f format] [-s search] +# Usage: group-list.sh [-f format] [-s search] [-a instance] # # Options: -# -f format Output format: table (default), json -# -s search Search by group name -# -h Show this help +# -f format Output format: table (default), json +# -s search Search by group name +# -a instance Authentik instance name (e.g. usc, mosaic) +# -h Show this help set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik FORMAT="table" SEARCH="" +AK_INSTANCE="" -while getopts "f:s:h" opt; do +while getopts "f:s:a:h" opt; do case $opt in f) FORMAT="$OPTARG" ;; s) SEARCH="$OPTARG" ;; - h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 [-f format] [-s search]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s search] [-a instance]" >&2; exit 1 ;; esac done -TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"}) PARAMS="ordering=name" [[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}" diff --git a/tools/authentik/user-create.sh b/tools/authentik/user-create.sh index e29511a..8d230d1 100755 --- a/tools/authentik/user-create.sh +++ b/tools/authentik/user-create.sh @@ -2,7 +2,7 @@ # # user-create.sh — Create an Authentik user # -# Usage: user-create.sh -u -n -e [-p password] [-g group] +# Usage: user-create.sh -u -n -e [-p password] [-g group] [-a instance] # # Options: # -u username Username (required) @@ -11,6 +11,7 @@ # -p password Initial password (optional — user gets set-password flow if omitted) # -g group Group name to add user to (optional) # -f format Output format: table (default), json +# -a instance Authentik instance name (e.g. usc, mosaic) # -h Show this help # # Environment variables (or credentials.json): @@ -20,11 +21,10 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik -USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table" +USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table" AK_INSTANCE="" -while getopts "u:n:e:p:g:f:h" opt; do +while getopts "u:n:e:p:g:f:a:h" opt; do case $opt in u) USERNAME="$OPTARG" ;; n) NAME="$OPTARG" ;; @@ -32,17 +32,24 @@ while getopts "u:n:e:p:g:f:h" opt; do p) PASSWORD="$OPTARG" ;; g) GROUP="$OPTARG" ;; f) FORMAT="$OPTARG" ;; - h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 -u -n -e [-p password] [-g group]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -19 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 -u -n -e [-p password] [-g group] [-a instance]" >&2; exit 1 ;; esac done +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + if [[ -z "$USERNAME" || -z "$NAME" || -z "$EMAIL" ]]; then echo "Error: -u username, -n name, and -e email are required" >&2 exit 1 fi -TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"}) # Build user payload payload=$(jq -n \ diff --git a/tools/authentik/user-list.sh b/tools/authentik/user-list.sh index 405e099..08932d9 100755 --- a/tools/authentik/user-list.sh +++ b/tools/authentik/user-list.sh @@ -2,13 +2,14 @@ # # user-list.sh — List Authentik users # -# Usage: user-list.sh [-f format] [-s search] [-g group] +# Usage: user-list.sh [-f format] [-s search] [-g group] [-a instance] # # Options: -# -f format Output format: table (default), json -# -s search Search term (matches username, name, email) -# -g group Filter by group name -# -h Show this help +# -f format Output format: table (default), json +# -s search Search term (matches username, name, email) +# -g group Filter by group name +# -a instance Authentik instance name (e.g. usc, mosaic) +# -h Show this help # # Environment variables (or credentials.json): # AUTHENTIK_URL — Authentik instance URL @@ -17,23 +18,30 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$MOSAIC_HOME/tools/_lib/credentials.sh" -load_credentials authentik FORMAT="table" SEARCH="" GROUP="" +AK_INSTANCE="" -while getopts "f:s:g:h" opt; do +while getopts "f:s:g:a:h" opt; do case $opt in f) FORMAT="$OPTARG" ;; s) SEARCH="$OPTARG" ;; g) GROUP="$OPTARG" ;; - h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; - *) echo "Usage: $0 [-f format] [-s search] [-g group]" >&2; exit 1 ;; + a) AK_INSTANCE="$OPTARG" ;; + h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s search] [-g group] [-a instance]" >&2; exit 1 ;; esac done -TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) +if [[ -n "$AK_INSTANCE" ]]; then + load_credentials "authentik-${AK_INSTANCE}" +else + load_credentials authentik +fi + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"}) # Build query params PARAMS="ordering=username"