feat: multi-instance Authentik credentials with test_user support

Add -a <instance> 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.<instance>.url      — instance URL
  authentik.<instance>.token    — API token (admin wrappers)
  authentik.<instance>.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-<name>).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-24 17:46:15 -06:00
parent 09786ee6e0
commit 21afb58b33
8 changed files with 152 additions and 70 deletions

View File

@@ -61,7 +61,8 @@ Usage: load_credentials <service>
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-<name> → 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[-<name>], cloudflare[-<name>]" >&2
echo "Supported: portainer, coolify, authentik[-<name>], glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-<name>], cloudflare[-<name>]" >&2
return 1
;;
esac

View File

@@ -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" \

View File

@@ -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}"

View File

@@ -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-<instance> (if valid)
# 2. Pre-configured token from credentials.json (authentik.<instance>.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 = \"<your-token>\"' credentials.json > tmp && mv tmp credentials.json" >&2
echo " Add token to credentials.json under authentik.<instance>.token" >&2
exit 1

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -2,7 +2,7 @@
#
# user-create.sh — Create an Authentik user
#
# Usage: user-create.sh -u <username> -n <name> -e <email> [-p password] [-g group]
# Usage: user-create.sh -u <username> -n <name> -e <email> [-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 <username> -n <name> -e <email> [-p password] [-g group]" >&2; exit 1 ;;
a) AK_INSTANCE="$OPTARG" ;;
h) head -19 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
*) echo "Usage: $0 -u <username> -n <name> -e <email> [-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 \

View File

@@ -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"