forked from coop-cloud/rauthy
- Added abra.sh functions for creating clients, groups and roles with the Rauthy API - Documentation and example for Nextcloud integration
244 lines
8.3 KiB
Bash
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
|
|
}
|