#!/bin/sh # Everything portal needs from this Kanidm, all the way: the OAuth2 # client people sign in through, one group per desk named exactly as # the content names it, and the service account portal onboards people # with - so that inviting someone from a desk, a decision that grants a # group, and the first person in (SEED_ADMIN_EMAIL) all simply work. # Rerunnable: each step skips what is already there. # # Logs in first if needed (interactive, idm_admin's password from # bootstrap.sh), writes the client secret and the onboarding token into # portal.env, and restarts portal. Needs the kanidm CLI and curl. set -eu cd "$(dirname "$0")" . ./.env K="kanidm -D idm_admin -H https://$ID_HOST" C=$OAUTH2_CLIENT_ID SA=portal-onboarding # Already signed in? Then do not ask again: this script is meant to be # rerunnable, and the session outlives a single run. $K self whoami >/dev/null 2>&1 || $K login has_client() { $K system oauth2 get "$1" 2>/dev/null | grep -q '^name:'; } has_group() { $K group get "$1" 2>/dev/null | grep -q '^name:'; } has_account() { $K service-account get "$1" 2>/dev/null | grep -q '^name:'; } # The desk groups are whatever the content gates pages on: every # `qualifies:` under questions/ in the content repo. Read from the repo, # so this list cannot drift from the pages. This Kanidm is the vel's # own, so a group is called exactly what the content calls it. host=$(printf '%s' "$CONTENT_REPO" | sed -E 's|^(https?://[^/]+)/.*|\1|') path=$(printf '%s' "$CONTENT_REPO" | sed -E 's|^https?://[^/]+/||') tree=$(curl -sf "$host/api/v1/repos/$path/git/trees/$CONTENT_BRANCH?recursive=true&per_page=1000") || { echo "could not read the content repo's file list ($CONTENT_REPO)" >&2; exit 1; } groups=$(printf '%s' "$tree" | grep -oE '"path":"questions/[^"]+\.yaml"' | sed -E 's/^"path":"//; s/"$//' | while read -r f; do curl -sf "$CONTENT_REPO/raw/branch/$CONTENT_BRANCH/$f" | sed -nE 's/^qualifies:[[:space:]]*"?([a-z0-9_-]+)"?[[:space:]]*$/\1/p' done | sort -u) [ -n "$groups" ] || { echo "the content gates no page on a group - nothing for a desk to sign in to" >&2; exit 1; } echo "desk groups from the content: $(echo $groups)" # The onboarding account. In idm_people_on_boarding it may create a # person and issue a first credential reset and nothing else, so its # token cannot touch anyone's existing credentials; idm_people_pii_read # lets an invite find someone who already has an account by their # email and add them instead of making a second account. Portal creates # the person with their address in the same call, because this account # may create but not modify. if ! has_account $SA; then $K service-account create $SA "Portal onboarding" idm_admin fi $K group add-members idm_people_on_boarding $SA >/dev/null $K group add-members idm_people_pii_read $SA >/dev/null || true # One group per desk, managed by the onboarding account: adding a # member to a group takes the right to manage that group, and a desk # invite or a `grants:` does exactly that. Groups made before this # script dropped its old `tomtervel_` prefix are renamed in place, so # their members and claim maps come along. for g in $groups; do if ! has_group "$g" && has_group "tomtervel_$g"; then $K group rename "tomtervel_$g" "$g" fi has_group "$g" || $K group create "$g" $SA $K group set-entry-manager "$g" $SA >/dev/null done # The people who ask the questions: everyone a page names as # responsible. Portal puts them here itself at start (RESPONSIBLE_GROUP); # the group only has to exist and be portal's to fill. Kanidm names are # ASCII, so "Redaktør" is its description. if [ -n "${RESPONSIBLE_GROUP:-}" ]; then has_group "$RESPONSIBLE_GROUP" || $K group create "$RESPONSIBLE_GROUP" $SA $K group set-entry-manager "$RESPONSIBLE_GROUP" $SA >/dev/null $K group set-description "$RESPONSIBLE_GROUP" "${RESPONSIBLE_GROUP_LABEL:-Redaktør}: står som ansvarlig for et spørsmål på siden" >/dev/null || true fi # The OAuth2 client. Everyone in this Kanidm is the vel's, so anyone # may sign in; what they see is decided by their desk groups, which # arrive in the `groups` claim under their own names. has_client $C || $K system oauth2 create $C "$SITE_NAME" "https://$PORTAL_HOST" $K system oauth2 add-redirect-url $C "https://$PORTAL_HOST/auth/callback" 2>/dev/null || true $K system oauth2 update-scope-map $C idm_all_persons openid profile email for g in $groups; do $K system oauth2 update-claim-map $C groups "$g" "$g" done $K system oauth2 update-claim-map-join $C groups array # The old umbrella group the scope map used to name, if this Kanidm # still has it: idm_all_persons does its job now. if has_group tomtervel_members; then $K system oauth2 delete-scope-map $C tomtervel_members 2>/dev/null || true $K group delete tomtervel_members fi # What a member sees when they land on the login page. Out of the box # Kanidm says "kanidm" and shows its own mark, which tells a neighbour # nothing about whose site they are signing in to. The logo comes from # the content repo, the same file site.yaml uses as the favicon. # # The client's name and logo are idm_admin's to set. The instance's own # (the login page's heading and mark) are system settings only `admin` # may change - idm_admin gets a bare "Item not found" - so that half # runs when an admin session exists, and otherwise says what to run. A="kanidm -D admin -H https://$ID_HOST" $K system oauth2 set-displayname $C "$SITE_NAME" >/dev/null || true logo=$(mktemp --suffix=.svg) if curl -sfL "$CONTENT_REPO/raw/branch/$CONTENT_BRANCH/images/logo.svg" -o "$logo" && [ -s "$logo" ]; then $K system oauth2 set-image $C "$logo" svg >/dev/null || echo "note: could not set the client logo" else echo "note: no images/logo.svg in the content repo - leaving the default mark" : > "$logo" fi if $A self whoami >/dev/null 2>&1; then $A system domain set-displayname "$SITE_NAME" >/dev/null || echo "note: could not set the login page's name" [ -s "$logo" ] && { $A system domain set-image "$logo" svg >/dev/null || echo "note: could not set the login page's logo"; } else echo "note: the login page's own name and logo need the admin account (bootstrap.sh printed its password):" echo " $A login # then rerun this script" fi rm -f "$logo" # Sign-in to a project Gitea for the people who ask the questions: its # own OAuth2 client, which only RESPONSIBLE_GROUP may use, carrying that # group in the `groups` claim so Gitea can require it as well. Gitea # does not do PKCE (Kanidm's own Gitea example turns it off), and short # usernames keep Gitea accounts "kari", not "kari@id.vel...". if [ -n "${GITEA_URL:-}" ] && [ -n "${RESPONSIBLE_GROUP:-}" ]; then G=${GITEA_OAUTH2_CLIENT_ID:-prosjekt} N=${GITEA_AUTH_NAME:-tomtervel} has_client $G || $K system oauth2 create $G "${GITEA_LABEL:-Prosjekt}" "$GITEA_URL/user/login" $K system oauth2 add-redirect-url $G "$GITEA_URL/user/oauth2/$N/callback" 2>/dev/null || true $K system oauth2 update-scope-map $G "$RESPONSIBLE_GROUP" openid profile email >/dev/null $K system oauth2 update-claim-map $G groups "$RESPONSIBLE_GROUP" "$RESPONSIBLE_GROUP" >/dev/null $K system oauth2 update-claim-map-join $G groups array >/dev/null $K system oauth2 warning-insecure-client-disable-pkce $G >/dev/null $K system oauth2 prefer-short-username $G >/dev/null gsecret=$($K system oauth2 show-basic-secret $G 2>/dev/null | tail -1) discover="https://$ID_HOST/oauth2/openid/$G/.well-known/openid-configuration" # On the host that runs that Gitea, add or update its source here; # anywhere else, leave the secret in a file only this user can read. if command -v gitea >/dev/null 2>&1 && sudo -n -u gitea true 2>/dev/null; then GT="sudo -n -u gitea gitea --config ${GITEA_CONFIG:-/etc/gitea/app.ini} admin auth" id=$($GT list 2>/dev/null | awk -v n="$N" '$2 == n { print $1 }') if [ -n "$id" ]; then $GT update-oauth --id "$id" --key "$G" --secret "$gsecret" --auto-discover-url "$discover" \ --required-claim-name groups --required-claim-value "$RESPONSIBLE_GROUP" --scopes "openid profile email" >/dev/null echo "Gitea sign-in source '$N' updated" else $GT add-oauth --name "$N" --provider openidConnect --key "$G" --secret "$gsecret" --auto-discover-url "$discover" \ --required-claim-name groups --required-claim-value "$RESPONSIBLE_GROUP" --scopes "openid profile email" >/dev/null echo "Gitea sign-in source '$N' added: $GITEA_URL/user/login has a 'Sign in with $N' button" fi else umask 077; printf '%s\n' "$gsecret" > gitea-oauth.secret echo "note: no gitea CLI here - on the Gitea host, add the source with the secret in gitea-oauth.secret:" echo " gitea admin auth add-oauth --name $N --provider openidConnect --key $G --secret \"\$(cat gitea-oauth.secret)\" \\" echo " --auto-discover-url $discover --required-claim-name groups --required-claim-value $RESPONSIBLE_GROUP --scopes 'openid profile email'" fi fi # Secrets into portal.env, never printed. The token is shown once, by # Kanidm, at creation; a rerun keeps the one portal.env already has. setenv() { if grep -q "^$1=" portal.env; then sed -i "s|^$1=.*|$1=$2|" portal.env; else printf '%s=%s\n' "$1" "$2" >> portal.env; fi; } secret=$($K system oauth2 show-basic-secret $C 2>/dev/null | tail -1) sed -i "s|^OAUTH2_CLIENT_SECRET=.*|OAUTH2_CLIENT_SECRET=$secret|" .env setenv OAUTH2_CLIENT_SECRET "$secret" if ! grep -q '^KANIDM_API_TOKEN=.' portal.env 2>/dev/null; then token=$($K service-account api-token generate $SA "portal" --readwrite 2>/dev/null | tail -1) if [ -n "$token" ]; then setenv KANIDM_API_TOKEN "$token" echo "onboarding token written to portal.env" else echo "warning: could not generate the onboarding token - invites will fail closed until it is set" >&2 echo " retry by hand: $K service-account api-token generate $SA portal --readwrite" >&2 fi fi # The first person in, if .env names one: portal invites them into the # most privileged group on its next start, once, and mails the link. if [ -n "${SEED_ADMIN_EMAIL:-}" ]; then setenv SEED_ADMIN_EMAIL "$SEED_ADMIN_EMAIL"; fi if [ -n "${SEED_ADMIN_NAME:-}" ]; then setenv SEED_ADMIN_NAME "$SEED_ADMIN_NAME"; fi if [ -n "${RESPONSIBLE_GROUP:-}" ]; then setenv RESPONSIBLE_GROUP "$RESPONSIBLE_GROUP"; fi podman compose up -d portal echo "portal restarted: signs in through $C, onboards through $SA, desks: $(echo $groups)" if [ -n "${SEED_ADMIN_EMAIL:-}" ]; then echo "the first person, $SEED_ADMIN_EMAIL, gets their invite from portal now (once)"; fi echo echo "Everyone after that is invited from a desk. By hand, if ever needed:" echo " $K group add-members styret "