Files
rauthy/abra.sh
Danny Groenewegen 38067eafc2 - Added bootstrapping admin password and API key
- Added abra.sh functions for creating clients, groups and roles with the Rauthy API
- Documentation and example for Nextcloud integration
2026-05-14 16:11:06 +02:00

244 lines
8.3 KiB
Bash

set -e
export CONFIG_TOML_VERSION=v5
generate_bootstrap_admin_password() {
if ! command -v argon2 &> /dev/null; then
echo "ERROR: 'argon2' CLI not found. Install it (e.g. 'apt install argon2')"
exit 1
fi
PASSWORD="$(openssl rand -base64 24)"
SALT="$(openssl rand -base64 24)"
HASH="$(echo -n "$PASSWORD" | argon2 "$SALT" -id -t 3 -m 16 -p 2 -l 32 -e)"
if abra app secret insert -C "$APP_NAME" admin_pwhash v1 "$HASH"; then
echo "Generated admin password:"
echo "$PASSWORD"
echo "WARNING: password is NOT shown again, please save it NOW"
else
echo "Failed to insert admin hash."
exit 1
fi
}
generate_enc_keys() {
KEY_A="$(openssl rand -base64 32)"
KEY_B="$(openssl rand -base64 32)"
abra app secret insert "$APP_NAME" enc_keys_a a1 "$KEY_A" --chaos
abra app secret insert "$APP_NAME" enc_keys_b b1 "$KEY_B" --chaos
echo "WARNING: secrets are NOT shown again, please save them NOW"
echo " enc_keys_a $KEY_A"
echo " enc_keys_b $KEY_B"
}
# Reads a Docker Swarm secret value from the running container
# Requires jq locally and SSH access to the server.
# Usage: get_secret <secret_name>
get_secret() {
local SECRET_NAME="$1"
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
local SERVER
SERVER=$(abra app ls -m | jq -r --arg domain "$APP_NAME" '[.[].apps[] | select(.domain == $domain) | .server] | first' 2>/dev/null)
if [ -z "$SERVER" ] || [ "$SERVER" = "null" ]; then
echo "ERROR: could not determine server for app '$APP_NAME'" >&2
exit 1
fi
local MATCH
MATCH=$(ssh "$SERVER" "
docker stack services ${STACK_NAME} --format '{{.Name}}' | while read svc; do
CID=\$(docker ps --no-trunc -q --filter \"name=\${svc}\" | head -1)
[ -z \"\$CID\" ] && continue
docker service inspect \"\$svc\" --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | jq -r --arg cid \"\$CID\" '.[]? | .SecretID + \" \" + \$cid + \" \" + .SecretName'
done
" 2>/dev/null | grep " ${STACK_NAME}_${SECRET_NAME}_" | head -1)
if [ -z "$MATCH" ]; then
echo "ERROR: secret '$SECRET_NAME' not found in stack '$STACK_NAME'" >&2
exit 1
fi
local SECRET_ID CID
SECRET_ID=$(echo "$MATCH" | awk '{print $1}')
CID=$(echo "$MATCH" | awk '{print $2}')
local VALUE
VALUE=$(ssh "$SERVER" "cat /var/lib/docker/containers/${CID}/mounts/secrets/${SECRET_ID} 2>/dev/null || sudo cat /var/lib/docker/containers/${CID}/mounts/secrets/${SECRET_ID} 2>/dev/null")
if [ -z "$VALUE" ]; then
echo "ERROR: could not read value for secret '$SECRET_NAME'" >&2
exit 1
fi
printf '%s' "$VALUE"
}
# Usage: rauthy_api_request <method> <path> [json_body]
# Sets globals API_HTTP_STATUS and API_BODY.
rauthy_api_request() {
local METHOD="$1" ENDPOINT="$2" PAYLOAD="${3:-}"
if [ -z "$API_SECRET" ]; then
API_SECRET=$(get_secret api_secret)
fi
local AUTH_HEADER
AUTH_HEADER=$(printf 'Authorization: API-Key bootstrap$%s' "$API_SECRET")
local ARGS=(-s -w "\n%{http_code}" -X "$METHOD" -H "$AUTH_HEADER")
[ -n "$PAYLOAD" ] && ARGS+=(-H "Content-Type: application/json" -d "$PAYLOAD")
local RESPONSE
RESPONSE=$(curl "${ARGS[@]}" "https://${DOMAIN}/auth/v1${ENDPOINT}")
API_HTTP_STATUS=$(echo "$RESPONSE" | tail -1)
API_BODY=$(echo "$RESPONSE" | sed '$d')
}
# Creates an OIDC client in Rauthy and prints the client secret.
# Usage: create_client <client_id> [insertsecret]
# Reads config from env vars prefixed with uppercased client_id:
# <ID>_CLIENT_NAME (required)
# <ID>_REDIRECT_URI (required)
# <ID>_ALLOWED_SCOPES (optional, default: 'email openid profile groups')
# With 'insertsecret': undeploys APP_NAME, replaces the Docker secret, then redeploys.
# Example: NEXTCLOUD_CLIENT_NAME="Nextcloud" NEXTCLOUD_REDIRECT_URI="https://..." create_client nextcloud
create_client() {
local CLIENT_ID="$1"
local MODE="$2"
if [ -z "$CLIENT_ID" ]; then
echo "ERROR: no client_id; Usage: create_client <client_id> [insertsecret]" >&2
exit 1
fi
local PREFIX
PREFIX=$(echo "$CLIENT_ID" | tr '[:lower:]' '[:upper:]')
local CLIENT_NAME REDIRECT_URI ALLOWED_SCOPES
CLIENT_NAME=$(eval "echo \"\${${PREFIX}_CLIENT_NAME}\"")
REDIRECT_URI=$(eval "echo \"\${${PREFIX}_REDIRECT_URI}\"")
ALLOWED_SCOPES=$(eval "echo \"\${${PREFIX}_ALLOWED_SCOPES:-email openid profile groups}\"")
if [ -z "$CLIENT_NAME" ] || [ -z "$REDIRECT_URI" ]; then
echo "ERROR: ${PREFIX}_CLIENT_NAME and ${PREFIX}_REDIRECT_URI must be set" >&2
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
rauthy_api_request GET "/clients/${CLIENT_ID}"
if [ "$API_HTTP_STATUS" = "200" ]; then
echo "Client '${CLIENT_ID}' already exists, skipping creation."
else
local PAYLOAD
PAYLOAD=$(jq -n \
--arg id "$CLIENT_ID" \
--arg name "$CLIENT_NAME" \
--arg redirect_uris "$REDIRECT_URI" \
--arg allowed_scopes "$ALLOWED_SCOPES" \
'$ARGS.named | .redirect_uris = [.redirect_uris] | .allowed_scopes = (.allowed_scopes | split(" ")) | .confidential = true')
rauthy_api_request POST "/clients" "$PAYLOAD"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to create client '${CLIENT_ID}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
fi
rauthy_api_request POST "/clients/${CLIENT_ID}/secret"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to fetch secret for client '${CLIENT_ID}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
local CLIENT_SECRET
CLIENT_SECRET=$(echo "$API_BODY" | jq -r '.secret // empty')
if [ -z "$CLIENT_SECRET" ]; then
echo "ERROR: no secret in API response for '${CLIENT_ID}'" >&2
echo "Response was: ${API_BODY}" >&2
exit 1
fi
if [ "$MODE" = "insertsecret" ]; then
echo "Undeploying '${APP_NAME}' to replace secret '${CLIENT_ID}_sec'"
abra --no-input app undeploy "$APP_NAME" || true
abra app secret remove -C "$APP_NAME" "${CLIENT_ID}_sec" || true
if printf '%s' "$CLIENT_SECRET" | abra app secret insert -C "$APP_NAME" "${CLIENT_ID}_sec" v1; then
echo "Secret '${CLIENT_ID}_sec' inserted, redeploying '${APP_NAME}'..."
else
echo "ERROR: failed to insert secret '${CLIENT_ID}_sec'; redeploying app" >&2
fi
abra --no-input app deploy -C "$APP_NAME" || true
#rauthy doesn't have a healthcheck, wait 5 seconds for startup
sleep 5
else
echo "Client '${CLIENT_ID}' created. Secret: ${CLIENT_SECRET}"
fi
}
# Creates one or more groups in Rauthy.
# Usage: create_groups <group_name> [<group_name> ...]
create_groups() {
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
rauthy_api_request GET "/groups"
local EXISTING_GROUPS
if [ "$API_HTTP_STATUS" = "200" ]; then
EXISTING_GROUPS=$(echo "$API_BODY" | jq -r '.[].name // empty')
fi
for GROUP_NAME in "$@"; do
if echo "$EXISTING_GROUPS" | grep -qx "$GROUP_NAME"; then
echo "Group '${GROUP_NAME}' already exists, skipping"
continue
fi
rauthy_api_request POST "/groups" "$(jq -n --arg group "$GROUP_NAME" '$ARGS.named')"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to create group '${GROUP_NAME}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
echo "Created group '${GROUP_NAME}'"
done
}
# Creates one or more roles in Rauthy.
# Usage: create_roles <role_name> [<role_name> ...]
create_roles() {
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
rauthy_api_request GET "/roles"
local EXISTING_ROLES
EXISTING_ROLES=$(echo "$API_BODY" | jq -r '.[].name // empty')
for ROLE_NAME in "$@"; do
if echo "$EXISTING_ROLES" | grep -qx "$ROLE_NAME"; then
echo "Role '${ROLE_NAME}' already exists, skipping"
continue
fi
rauthy_api_request POST "/roles" "$(jq -n --arg role "$ROLE_NAME" '$ARGS.named')"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to create role '${ROLE_NAME}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
echo "Created role '${ROLE_NAME}'"
done
}