Merge pull request #360 from outis1one/claude/gitea-standalone-setup-oxoi2e
Claude/gitea standalone setup oxoi2e
This commit is contained in:
@@ -198,6 +198,73 @@ fi
|
||||
|
||||
register_service actualbudget utilities "Open-source personal finance & budgeting (Actual Budget)" 5006
|
||||
|
||||
# Offers to add "Sign in with Authelia" (OpenID Connect) to Actual Budget's
|
||||
# own login page — same additive pattern as services/gitea.sh's
|
||||
# _gitea_offer_authelia_sso, entirely environment-variable driven like
|
||||
# services/mealie.sh's equivalent. Confirmed against Actual Budget's own
|
||||
# OIDC docs: ACTUAL_OPENID_DISCOVERY_URL, ACTUAL_OPENID_CLIENT_ID,
|
||||
# ACTUAL_OPENID_CLIENT_SECRET, ACTUAL_OPENID_SERVER_HOSTNAME, appended
|
||||
# into the .env file this installer already writes and reads via
|
||||
# `env_file: .env`. Redirect path (/openid/callback) matches the preset
|
||||
# already used by services/authelia.sh's own "Register an app" menu for
|
||||
# this same app, so both stay consistent with each other.
|
||||
#
|
||||
# No stored BASE_URL to read back here (unlike Mealie) — Actual Budget's
|
||||
# compose/.env never records the public URL, so this asks for the domain
|
||||
# directly instead, same as services/gitea.sh's SSO offer does.
|
||||
#
|
||||
# Args: DIR
|
||||
_actualbudget_offer_authelia_oidc() {
|
||||
local DIR="$1"
|
||||
|
||||
[ -d "$DOCKER_DIR/authelia" ] || return 0
|
||||
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
|
||||
grep -q '^ACTUAL_OPENID_DISCOVERY_URL=' "$DIR/.env" 2>/dev/null && return 0
|
||||
|
||||
echo ""
|
||||
local USE_SSO=""
|
||||
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Actual Budget's login page? (y/n):" "n" USE_SSO
|
||||
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
|
||||
|
||||
local _default_domain=""
|
||||
[ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ] && _default_domain="budget.${SITE_DOMAIN}"
|
||||
local AB_OIDC_DOMAIN=""
|
||||
prompt_text " Domain Actual Budget is reachable at [${_default_domain:-required}]:" "$_default_domain" AB_OIDC_DOMAIN
|
||||
if [ -z "$AB_OIDC_DOMAIN" ]; then
|
||||
log_warning "No domain entered — skipping Authelia SSO for Actual Budget."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local _2fa="" AUTH_POLICY="two_factor"
|
||||
prompt_yn " Require two-factor for Actual Budget logins via Authelia too? (y/n):" "y" _2fa
|
||||
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
|
||||
|
||||
if ! _authelia_provision_oidc_client "ActualBudget" "actualbudget" "$AUTH_POLICY" "y" \
|
||||
"https://${AB_OIDC_DOMAIN}/openid/callback"; then
|
||||
log_warning "Couldn't register Actual Budget as an OIDC client in Authelia — skipping SSO setup."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local _discovery_url="https://auth.${OIDC_AUTHELIA_DOMAIN}/.well-known/openid-configuration"
|
||||
cat >> "$DIR/.env" << ENV
|
||||
|
||||
# Written by services/actualbudget.sh's Authelia SSO step. The first OIDC
|
||||
# login becomes the Actual Budget server owner if no owner is set yet —
|
||||
# that's Actual Budget's own behavior, not something this script controls.
|
||||
ACTUAL_OPENID_DISCOVERY_URL=$_discovery_url
|
||||
ACTUAL_OPENID_CLIENT_ID=actualbudget
|
||||
ACTUAL_OPENID_CLIENT_SECRET=$OIDC_CLIENT_SECRET_PLAIN
|
||||
ACTUAL_OPENID_SERVER_HOSTNAME=https://${AB_OIDC_DOMAIN}
|
||||
ENV
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true
|
||||
|
||||
(cd "$DIR" && docker compose up -d) \
|
||||
&& log_success "\"Sign in with Authelia\" added to Actual Budget — local login still works too." \
|
||||
|| log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"
|
||||
|
||||
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "actualbudget" "$AB_OIDC_DOMAIN"
|
||||
}
|
||||
|
||||
install_actualbudget() {
|
||||
require_docker || return 1
|
||||
|
||||
@@ -216,6 +283,7 @@ install_actualbudget() {
|
||||
echo " - Create \$DOCKER_DIR/actualbudget(-<name>) with docker-compose.yml (data/)"
|
||||
echo " - Auto-scan for a free host port if this is an additional instance"
|
||||
echo " - Offer a Caddy reverse proxy and to start the container"
|
||||
echo " - Offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -258,6 +326,7 @@ install_actualbudget() {
|
||||
( cd "$AB_DIR" && docker compose pull && docker compose up -d ) \
|
||||
&& log_success "Actual Budget image refreshed" \
|
||||
|| log_warning "Refresh failed — check: docker compose -f $AB_DIR/docker-compose.yml logs"
|
||||
declare -F _actualbudget_offer_authelia_oidc >/dev/null 2>&1 && _actualbudget_offer_authelia_oidc "$AB_DIR"
|
||||
return 0
|
||||
;;
|
||||
cancel)
|
||||
@@ -333,6 +402,8 @@ AB_ENV
|
||||
|
||||
configure_caddy_for_service "ActualBudget${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:5006" "budget${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
|
||||
|
||||
declare -F _actualbudget_offer_authelia_oidc >/dev/null 2>&1 && _actualbudget_offer_authelia_oidc "$AB_DIR"
|
||||
|
||||
write_readme "$AB_DIR" << MD
|
||||
# Actual Budget${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
|
||||
|
||||
|
||||
+356
-6
@@ -232,10 +232,12 @@ install_authelia() {
|
||||
echo " Vaultwarden, or any other app with its own \"Enable OpenID\" setting)"
|
||||
echo " 5) Reconfigure from scratch (regenerates secrets/users — breaks"
|
||||
echo " existing sessions for every domain already on this instance)"
|
||||
echo " 6) Leave as-is"
|
||||
echo " 6) Show who has universal vs. service-scoped access"
|
||||
echo " 7) Change \"Remember me\" session duration (stay logged in longer)"
|
||||
echo " 8) Leave as-is"
|
||||
echo ""
|
||||
local EXISTING_CHOICE=""
|
||||
prompt_text " Choice [1/2/3/4/5/6]:" "6" EXISTING_CHOICE
|
||||
prompt_text " Choice [1/2/3/4/5/6/7/8]:" "8" EXISTING_CHOICE
|
||||
case "$EXISTING_CHOICE" in
|
||||
1)
|
||||
add_authelia_domain
|
||||
@@ -256,6 +258,14 @@ install_authelia() {
|
||||
5)
|
||||
: # fall through to the full reinstall flow below
|
||||
;;
|
||||
6)
|
||||
_authelia_report_access_scope
|
||||
return 0
|
||||
;;
|
||||
7)
|
||||
_authelia_set_remember_me
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
|
||||
return 0
|
||||
@@ -915,6 +925,310 @@ _authelia_toggle_admin() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Same shape as _authelia_toggle_admin but for an arbitrary group name —
|
||||
# used to scope a user's access to a single service (see
|
||||
# _authelia_scope_access below) rather than the fixed "admins" group.
|
||||
_authelia_toggle_group() {
|
||||
local users_file="$1" start="$2" end="$3" group="$4" enable="$5"
|
||||
if [ "$enable" = "true" ]; then
|
||||
if ! sed -n "${start},${end}p" "$users_file" | grep -qF " - ${group}"; then
|
||||
awk -v s="$start" -v e="$end" -v grp=" - ${group}" '
|
||||
{ print }
|
||||
NR>=s && NR<=e && /^ groups:$/ { print grp }
|
||||
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
|
||||
fi
|
||||
else
|
||||
awk -v s="$start" -v e="$end" -v grpline=" - ${group}" '
|
||||
NR>=s && NR<=e && $0==grpline { next }
|
||||
{ print }
|
||||
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Non-interactive core of add_authelia_user() below — no prompts, takes
|
||||
# everything as args, generates a temp password + hash, and writes the user
|
||||
# block directly into an arbitrary extra group (not just "users"). Used by
|
||||
# _authelia_scope_access() to create users on the fly when someone lists a
|
||||
# username that doesn't exist yet. Deliberately a separate function rather
|
||||
# than a refactor of add_authelia_user() itself — that one's already in
|
||||
# regular use via the interactive menu and this repo's convention is to
|
||||
# extract a non-interactive core only when a second caller actually needs
|
||||
# it (see _authelia_provision_oidc_client for the same reasoning), which
|
||||
# keeps this addition low-risk to the existing, working function.
|
||||
#
|
||||
# Args: USERNAME DISPLAY EMAIL GROUP
|
||||
# Out-param (not `local`): AUTHELIA_NEW_USER_TEMP_PASSWORD
|
||||
# Returns 1 if the user already exists or hash generation fails.
|
||||
_authelia_create_user_noninteractive() {
|
||||
local username="$1" display="$2" email="$3" group="$4"
|
||||
local users_file="$DOCKER_DIR/authelia/config/users.yml"
|
||||
|
||||
AUTHELIA_NEW_USER_TEMP_PASSWORD=""
|
||||
|
||||
if grep -qE "^ ${username}:$" "$users_file" 2>/dev/null; then
|
||||
log_warning "'$username' already exists in $users_file."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local temp_pass new_hash
|
||||
temp_pass="$(_authelia_gen_temp_password)"
|
||||
new_hash=$(docker run --rm authelia/authelia:4.39.20 \
|
||||
authelia crypto hash generate argon2 --password "$temp_pass" 2>/dev/null \
|
||||
| grep -oP '(?<=Digest: ).*')
|
||||
if [ -z "$new_hash" ]; then
|
||||
log_warning "Couldn't generate a password hash for '$username' automatically."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local user_block=" ${username}:
|
||||
displayname: \"${display}\"
|
||||
email: ${email}
|
||||
password: \"${new_hash}\"
|
||||
groups:
|
||||
- ${group}"
|
||||
|
||||
awk -v block="$user_block" '
|
||||
{ print }
|
||||
/^users:$/ && !done { print block; done=1 }
|
||||
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
|
||||
chown 1000:1000 "$users_file" 2>/dev/null || true
|
||||
|
||||
AUTHELIA_NEW_USER_TEMP_PASSWORD="$temp_pass"
|
||||
log_success "Created user '$username' (group: $group)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Reusable by ANY service, after it's already been protected by Authelia —
|
||||
# forward_auth gate or native OIDC alike, since this only cares about the
|
||||
# domain, not the gating mechanism. Asks whether access to $DOMAIN should be
|
||||
# open to any Authelia user (today's only behavior, before this existed) or
|
||||
# scoped to a specific list. If scoped: creates a dedicated group named
|
||||
# "<service_id>-only", adds every listed username to it (creating any that
|
||||
# don't exist yet via _authelia_create_user_noninteractive), and inserts two
|
||||
# access_control rules ABOVE the general catch-all — allow this group on
|
||||
# $DOMAIN, deny this group on every other protected domain on the instance —
|
||||
# so members can reach ONLY this one domain. Idempotent: reruns against a
|
||||
# domain that's already scoped just report the existing group instead of
|
||||
# duplicating rules.
|
||||
#
|
||||
# Args: SERVICE_ID DOMAIN
|
||||
_authelia_scope_access() {
|
||||
local service_id="$1" domain="$2"
|
||||
local authelia_dir="$DOCKER_DIR/authelia"
|
||||
local config_file="$authelia_dir/config/configuration.yml"
|
||||
local users_file="$authelia_dir/config/users.yml"
|
||||
|
||||
[ -f "$config_file" ] || return 0
|
||||
|
||||
local group="${service_id}-only"
|
||||
|
||||
if grep -qF "subject: \"group:${group}\"" "$config_file" 2>/dev/null; then
|
||||
log_info "Access to $domain is already scoped to group '$group'."
|
||||
log_info "Manage its members via this menu's \"Edit an existing user\" (toggle their groups by hand in users.yml), or the universal-access report below."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Who should be able to reach $domain via Authelia?"
|
||||
echo " 1) Any Authelia user (default — same access as everything else)"
|
||||
echo " 2) Specific users only"
|
||||
local scope_choice=""
|
||||
prompt_text " Choice [1/2]:" "1" scope_choice
|
||||
[ "$scope_choice" = "2" ] || return 0
|
||||
|
||||
echo " Usernames who should have access (space-separated). Anyone listed"
|
||||
echo " who doesn't already have an Authelia account gets one created —"
|
||||
echo " you'll get their temporary password to hand over."
|
||||
local raw_users=""
|
||||
prompt_text " Usernames:" "" raw_users
|
||||
local -a usernames
|
||||
read -ra usernames <<< "$raw_users"
|
||||
if [ "${#usernames[@]}" -eq 0 ]; then
|
||||
log_warning "No usernames entered — leaving $domain open to all Authelia users."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local u start_end start end
|
||||
for u in "${usernames[@]}"; do
|
||||
u="$(echo "$u" | tr -cs 'a-z0-9_-' '-' | sed 's/^-*//;s/-*$//')"
|
||||
[ -z "$u" ] && continue
|
||||
if grep -qE "^ ${u}:$" "$users_file" 2>/dev/null; then
|
||||
start_end="$(_authelia_user_line_range "$users_file" "$u")"
|
||||
start="${start_end% *}"; end="${start_end#* }"
|
||||
_authelia_toggle_group "$users_file" "$start" "$end" "$group" "true"
|
||||
log_success "Added '$u' to group '$group'"
|
||||
else
|
||||
local email_default="${u}@${SITE_DOMAIN:-example.com}"
|
||||
if _authelia_create_user_noninteractive "$u" "$u" "$email_default" "$group"; then
|
||||
echo " Temp password for '$u': $AUTHELIA_NEW_USER_TEMP_PASSWORD"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Two rules, both above the general catch-all: allow this group on the
|
||||
# target domain, deny this group on every other protected domain. Order
|
||||
# matters — Authelia takes the first matching rule, so both must land
|
||||
# before access_control's existing "*.${AUTHELIA_DOMAIN}" catch-all.
|
||||
local authelia_domain
|
||||
authelia_domain="$(awk '/^ cookies:$/{f=1; next} f && /domain:/{print $3; exit}' "$config_file")"
|
||||
local scope_rules=" - domain: \"${domain}\"
|
||||
subject: \"group:${group}\"
|
||||
policy: two_factor
|
||||
- domain: \"*.${authelia_domain}\"
|
||||
subject: \"group:${group}\"
|
||||
policy: deny"
|
||||
|
||||
awk -v block="$scope_rules" '
|
||||
/^ rules:$/ && !done { print; print block; done=1; next }
|
||||
{ print }
|
||||
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
|
||||
local restart_auth=""
|
||||
prompt_yn " Restart Authelia to apply this scoping? (y/n):" "y" restart_auth
|
||||
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
|
||||
(cd "$authelia_dir" && docker compose restart authelia 2>/dev/null) \
|
||||
&& log_success "Authelia restarted — $domain is now restricted to group '$group'." \
|
||||
|| log_warning "Restart failed — check: docker compose logs authelia"
|
||||
fi
|
||||
}
|
||||
|
||||
# Reporting/management: lists which users have "universal" access (every
|
||||
# protected domain — anyone not locked into a "<service>-only" group) versus
|
||||
# which are scoped to specific services, then offers to promote a scoped
|
||||
# user to universal by removing them from all their "-only" groups. Doesn't
|
||||
# touch access_control.rules at all — universal access is just the absence
|
||||
# of a restricting group, so "promoting" someone is purely a users.yml edit.
|
||||
_authelia_report_access_scope() {
|
||||
local users_file="$DOCKER_DIR/authelia/config/users.yml"
|
||||
[ -f "$users_file" ] || { log_warning "No users.yml found — install Authelia first."; return 1; }
|
||||
|
||||
local -a all_users
|
||||
mapfile -t all_users < <(_authelia_list_usernames "$users_file")
|
||||
if [ "${#all_users[@]}" -eq 0 ]; then
|
||||
log_warning "No users found in $users_file."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Universal access (every protected domain):"
|
||||
local -a universal=() restricted=()
|
||||
local u start_end start end groups_in_range
|
||||
for u in "${all_users[@]}"; do
|
||||
start_end="$(_authelia_user_line_range "$users_file" "$u")"
|
||||
start="${start_end% *}"; end="${start_end#* }"
|
||||
groups_in_range="$(sed -n "${start},${end}p" "$users_file" | grep -oE '\- [a-z0-9_-]+-only$' | sed 's/^- //')"
|
||||
if [ -z "$groups_in_range" ]; then
|
||||
universal+=("$u")
|
||||
echo " - $u"
|
||||
else
|
||||
restricted+=("$u ($(echo "$groups_in_range" | tr '\n' ',' | sed 's/,$//'))")
|
||||
fi
|
||||
done
|
||||
[ "${#universal[@]}" -eq 0 ] && echo " (none)"
|
||||
|
||||
echo ""
|
||||
echo " Scoped to specific services only:"
|
||||
if [ "${#restricted[@]}" -eq 0 ]; then
|
||||
echo " (none)"
|
||||
else
|
||||
printf ' - %s\n' "${restricted[@]}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local promote=""
|
||||
prompt_yn " Promote a scoped user to universal access? (y/n):" "n" promote
|
||||
[[ "$promote" =~ ^[Yy]$ ]] || return 0
|
||||
|
||||
local target=""
|
||||
prompt_text " Username to promote:" "" target
|
||||
[ -z "$target" ] && return 0
|
||||
if ! grep -qE "^ ${target}:$" "$users_file" 2>/dev/null; then
|
||||
log_warning "'$target' not found in $users_file."
|
||||
return 0
|
||||
fi
|
||||
|
||||
start_end="$(_authelia_user_line_range "$users_file" "$target")"
|
||||
start="${start_end% *}"; end="${start_end#* }"
|
||||
local -a target_groups
|
||||
mapfile -t target_groups < <(sed -n "${start},${end}p" "$users_file" | grep -oE '\- [a-z0-9_-]+-only$' | sed 's/^- //')
|
||||
if [ "${#target_groups[@]}" -eq 0 ]; then
|
||||
log_info "'$target' already has universal access."
|
||||
return 0
|
||||
fi
|
||||
local g
|
||||
for g in "${target_groups[@]}"; do
|
||||
_authelia_toggle_group "$users_file" "$start" "$end" "$g" "false"
|
||||
done
|
||||
log_success "'$target' removed from: ${target_groups[*]} — now has universal access."
|
||||
|
||||
local restart_auth=""
|
||||
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
|
||||
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
|
||||
(cd "$DOCKER_DIR/authelia" && docker compose restart authelia 2>/dev/null) \
|
||||
&& log_success "Authelia restarted" \
|
||||
|| log_warning "Restart failed — check: docker compose logs authelia"
|
||||
fi
|
||||
}
|
||||
|
||||
# Changes how long an Authelia session lasts when a user checks "Remember
|
||||
# me" at login — the actual mechanism behind "log in once, don't get asked
|
||||
# again for a long time" for every domain this instance protects.
|
||||
#
|
||||
# The config key is `remember_me` (plain, under session:), NOT
|
||||
# `remember_me_duration` — that name was retired in Authelia 4.38, this
|
||||
# repo pins 4.39.20. Confirmed against Authelia's own docs/changelog
|
||||
# before writing this; an easy mistake since older guidance (including an
|
||||
# earlier version of this very file's own README section) uses the old
|
||||
# name, which Authelia would just silently ignore rather than error on.
|
||||
#
|
||||
# This only controls AUTHELIA's own session — it does not touch how long
|
||||
# a native-OIDC app's (Gitea/Mealie/ActualBudget) own session/token lasts
|
||||
# after logging in via Authelia. A long remember_me makes re-authenticating
|
||||
# to Authelia itself instant/silent whenever one of those apps' own
|
||||
# session expires and sends you back through the OIDC flow, but doesn't
|
||||
# stop that app's own session from expiring on its own separate schedule.
|
||||
_authelia_set_remember_me() {
|
||||
local config_file="$DOCKER_DIR/authelia/config/configuration.yml"
|
||||
[ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; }
|
||||
|
||||
local current
|
||||
current="$(grep -E '^ remember_me:' "$config_file" | awk '{print $2}' | tr -d "'\"")"
|
||||
echo ""
|
||||
echo " Current \"remember me\" duration: ${current:-not set}"
|
||||
echo " How long a session lasts when someone checks \"Remember me\" at login —"
|
||||
echo " applies to every domain this Authelia instance protects."
|
||||
echo " Examples: 12h, 7d, 1M (month), 1y. Set to -1 to disable Remember Me entirely."
|
||||
local new_duration=""
|
||||
prompt_text " New duration [${current:-7d}]:" "${current:-7d}" new_duration
|
||||
if [ -z "$new_duration" ] || [ "$new_duration" = "$current" ]; then
|
||||
log_info "No change made."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -qE '^ remember_me:' "$config_file"; then
|
||||
sed -i "s/^ remember_me:.*/ remember_me: '${new_duration}'/" "$config_file"
|
||||
else
|
||||
sed -i "/^session:\$/a\\ remember_me: '${new_duration}'" "$config_file"
|
||||
fi
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
log_success "\"Remember me\" duration set to ${new_duration}."
|
||||
|
||||
local restart_auth=""
|
||||
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
|
||||
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
|
||||
(cd "$DOCKER_DIR/authelia" && docker compose restart authelia 2>/dev/null) \
|
||||
&& log_success "Authelia restarted" \
|
||||
|| log_warning "Restart failed — check: docker compose logs authelia"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "Takes effect for NEW logins where \"Remember me\" is checked at Authelia's"
|
||||
log_info "login page — existing sessions keep whatever expiration they already had."
|
||||
log_info "The checkbox itself is already on the login form by default; this only"
|
||||
log_info "changes how long checking it actually keeps you signed in."
|
||||
}
|
||||
|
||||
# action="exempt": inserts a "policy: one_factor / subject: user:<name>" rule
|
||||
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
|
||||
# configuration.yml (handles multi-domain instances from add_authelia_domain
|
||||
@@ -1190,6 +1504,30 @@ _authelia_ensure_oidc_provider() {
|
||||
log_success "OIDC provider enabled (signing key + HMAC secret generated)"
|
||||
}
|
||||
|
||||
# Deletes one OIDC client block (matched by client_id) from
|
||||
# identity_providers.oidc.clients in configuration.yml. Used by
|
||||
# _authelia_provision_oidc_client below to make re-registering a client_id
|
||||
# idempotent instead of a dead end — see that function's own comment on
|
||||
# why a stale registration is safe to just replace. A client block starts
|
||||
# at its own " - client_id: '<id>'" line (6-space indent) and runs
|
||||
# until either the next such line or a line indented less than 6 spaces
|
||||
# (end of the clients list) — deleting stops exactly there so a sibling
|
||||
# client's block, or whatever config section follows, is untouched.
|
||||
_authelia_remove_oidc_client() {
|
||||
local config_file="$1" client_id="$2"
|
||||
awk -v target="'${client_id}'" '
|
||||
{
|
||||
if ($0 ~ /^ - client_id: /) {
|
||||
skip = ($0 ~ target) ? 1 : 0
|
||||
} else if (skip && $0 !~ /^ /) {
|
||||
skip = 0
|
||||
}
|
||||
if (!skip) print
|
||||
}
|
||||
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Non-interactive core of _authelia_add_oidc_client() below — generates a
|
||||
# client secret, patches it into identity_providers.oidc.clients, and
|
||||
# (optionally) restarts Authelia. Fully self-contained (re-validates
|
||||
@@ -1207,8 +1545,10 @@ _authelia_ensure_oidc_provider() {
|
||||
# caller must capture and use/display it now.
|
||||
# OIDC_AUTHELIA_DOMAIN this Authelia instance's apex domain, for
|
||||
# building discovery/authorization/token URLs.
|
||||
# Returns 1 on failure (Authelia not installed, client ID already taken,
|
||||
# secret generation failed) with the reason already logged.
|
||||
# Returns 1 on failure (Authelia not installed, domain undeterminable,
|
||||
# secret generation failed) with the reason already logged. A client_id
|
||||
# that's already registered is NOT a failure — it gets replaced (see the
|
||||
# comment at that check below).
|
||||
_authelia_provision_oidc_client() {
|
||||
local APP_NAME="$1" CLIENT_ID="$2" AUTH_POLICY="$3" RESTART_AUTH="$4"; shift 4
|
||||
local -a REDIRECT_URIS=("$@")
|
||||
@@ -1239,9 +1579,19 @@ _authelia_provision_oidc_client() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# A stale registration (e.g. from the interactive "Register an app" menu
|
||||
# run previously without ever finishing — its plaintext secret was shown
|
||||
# once and is gone, so the registration is dead weight either way) would
|
||||
# otherwise permanently block this exact service's automated SSO offer
|
||||
# with nothing but a warning. Confirmed live: this is what happened to
|
||||
# ActualBudget the first time its own offer ran, against a client_id the
|
||||
# menu had already registered in an earlier session. Safe to just
|
||||
# replace — every automated caller here uses a fixed, service-specific
|
||||
# client_id, so a collision means "this same service, already
|
||||
# registered" rather than someone else's app using the same ID.
|
||||
if grep -qF "client_id: '${CLIENT_ID}'" "$CONFIG_FILE" 2>/dev/null; then
|
||||
log_warning "A client with ID '$CLIENT_ID' is already registered in $CONFIG_FILE."
|
||||
return 1
|
||||
log_warning "A client with ID '$CLIENT_ID' is already registered — replacing it with a fresh one (its old secret was never recoverable anyway)."
|
||||
_authelia_remove_oidc_client "$CONFIG_FILE" "$CLIENT_ID"
|
||||
fi
|
||||
|
||||
log_info "Generating client secret..."
|
||||
|
||||
@@ -236,6 +236,8 @@ _gitea_offer_authelia_sso() {
|
||||
log_warning " Discovery URL: $_discovery_url"
|
||||
log_warning " (The Client Secret above is shown once — it isn't stored in plaintext anywhere.)"
|
||||
fi
|
||||
|
||||
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "gitea" "$GITEA_OIDC_DOMAIN"
|
||||
}
|
||||
|
||||
# Offers to enable Gitea Actions (Gitea's own CI, largely GitHub-Actions-
|
||||
|
||||
@@ -198,6 +198,83 @@ fi
|
||||
|
||||
register_service mealie utilities "Recipe manager & meal planner (Mealie)" 9925
|
||||
|
||||
# Offers to add "Sign in with Authelia" (OpenID Connect) to Mealie's own
|
||||
# login page — same additive pattern as services/gitea.sh's
|
||||
# _gitea_offer_authelia_sso (local login keeps working unchanged), but
|
||||
# Mealie's OIDC support is entirely environment-variable driven — no CLI
|
||||
# equivalent to Gitea's `admin auth add-oauth` needed. Confirmed against
|
||||
# Mealie's own OIDC docs: OIDC_AUTH_ENABLED, OIDC_CLIENT_ID,
|
||||
# OIDC_CLIENT_SECRET, OIDC_CONFIGURATION_URL, OIDC_SIGNUP_ENABLED, appended
|
||||
# straight into the .env file this installer already writes and reads via
|
||||
# `env_file: .env` — no docker-compose.yml regeneration needed for that part.
|
||||
#
|
||||
# Reads BASE_URL back from the existing .env rather than taking it as an
|
||||
# arg, so this works identically whether called right after a fresh
|
||||
# install (where the URL was just computed) or from an Update rerun
|
||||
# (where it wasn't recomputed this run, but is already on disk).
|
||||
#
|
||||
# Args: DIR CONTAINER
|
||||
_mealie_offer_authelia_oidc() {
|
||||
local DIR="$1" CONTAINER="$2"
|
||||
|
||||
[ -d "$DOCKER_DIR/authelia" ] || return 0
|
||||
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
|
||||
grep -q '^OIDC_AUTH_ENABLED=' "$DIR/.env" 2>/dev/null && return 0
|
||||
|
||||
local BASE_URL
|
||||
BASE_URL="$(grep '^BASE_URL=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)"
|
||||
if [ -z "$BASE_URL" ]; then
|
||||
log_warning "Couldn't find BASE_URL in $DIR/.env — skipping Authelia SSO offer for Mealie."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local USE_SSO=""
|
||||
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Mealie's login page? (y/n):" "n" USE_SSO
|
||||
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
|
||||
|
||||
local _2fa="" AUTH_POLICY="two_factor"
|
||||
prompt_yn " Require two-factor for Mealie logins via Authelia too? (y/n):" "y" _2fa
|
||||
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
|
||||
|
||||
if ! _authelia_provision_oidc_client "Mealie" "mealie" "$AUTH_POLICY" "y" "${BASE_URL}/login"; then
|
||||
log_warning "Couldn't register Mealie as an OIDC client in Authelia — skipping SSO setup."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local _discovery_url="https://auth.${OIDC_AUTHELIA_DOMAIN}/.well-known/openid-configuration"
|
||||
cat >> "$DIR/.env" << ENV
|
||||
|
||||
# Written by services/mealie.sh's Authelia SSO step — adds "Sign in with
|
||||
# Authelia" alongside local login; local accounts keep working unchanged.
|
||||
OIDC_AUTH_ENABLED=true
|
||||
OIDC_SIGNUP_ENABLED=true
|
||||
OIDC_CLIENT_ID=mealie
|
||||
OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET_PLAIN
|
||||
OIDC_CONFIGURATION_URL=$_discovery_url
|
||||
OIDC_PROVIDER_NAME=Authelia
|
||||
ENV
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true
|
||||
|
||||
# Mealie's OIDC redirect URI generation trusts X-Forwarded-* only from
|
||||
# explicitly allowed IPs — without this, a Caddy-fronted instance
|
||||
# generates an http:// redirect URI even when actually served over
|
||||
# https://, which Authelia/any OIDC provider rejects as a scheme
|
||||
# mismatch. Confirmed against Mealie's own reverse-proxy docs/issue
|
||||
# tracker. Only needed (and only added) when Caddy is actually
|
||||
# fronting this instance — BASE_URL itself tells us that (it's only
|
||||
# ever https:// when a real domain + Caddy were configured).
|
||||
if [[ "$BASE_URL" == https://* ]] && ! grep -q '^ entrypoint:' "$DIR/docker-compose.yml"; then
|
||||
sed -i "/container_name: ${CONTAINER}\$/a\\ entrypoint: [\"uvicorn\", \"mealie.app:app\", \"--host\", \"0.0.0.0\", \"--port\", \"9000\", \"--forwarded-allow-ips=*\"]" "$DIR/docker-compose.yml"
|
||||
fi
|
||||
|
||||
(cd "$DIR" && docker compose up -d) \
|
||||
&& log_success "\"Sign in with Authelia\" added to Mealie — local login still works too." \
|
||||
|| log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"
|
||||
|
||||
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "mealie" "${BASE_URL#*://}"
|
||||
}
|
||||
|
||||
install_mealie() {
|
||||
require_docker || return 1
|
||||
|
||||
@@ -217,6 +294,7 @@ install_mealie() {
|
||||
echo " - Auto-scan for a free host port if this is an additional instance"
|
||||
echo " - Default login: changeme@email.com / MyPassword (change immediately)"
|
||||
echo " - Offer a Caddy reverse proxy and to start the container"
|
||||
echo " - Offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -259,6 +337,7 @@ install_mealie() {
|
||||
( cd "$MEALIE_DIR" && docker compose pull && docker compose up -d ) \
|
||||
&& log_success "Mealie image refreshed" \
|
||||
|| log_warning "Refresh failed — check: docker compose -f $MEALIE_DIR/docker-compose.yml logs"
|
||||
declare -F _mealie_offer_authelia_oidc >/dev/null 2>&1 && _mealie_offer_authelia_oidc "$MEALIE_DIR" "$CONTAINER"
|
||||
return 0
|
||||
;;
|
||||
cancel)
|
||||
@@ -352,6 +431,8 @@ MEALIE_ENV
|
||||
|
||||
configure_caddy_for_service "Mealie${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:9000" "recipes${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
|
||||
|
||||
declare -F _mealie_offer_authelia_oidc >/dev/null 2>&1 && _mealie_offer_authelia_oidc "$MEALIE_DIR" "$CONTAINER"
|
||||
|
||||
write_readme "$MEALIE_DIR" << MD
|
||||
# Mealie${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user