diff --git a/CLAUDE.md b/CLAUDE.md index e9e76f7..b852a9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -422,22 +422,49 @@ existing login page. Reuse `_authelia_provision_oidc_client()` (guarded by instead of duplicating Authelia's client-secret-generation/config-patching logic again. -**Scoping a domain to specific users instead of every Authelia user.** -By default, any domain with an `access_control` rule at all is reachable by -every Authelia user (the existing catch-all `*.${AUTHELIA_DOMAIN}` rule). -`services/authelia.sh`'s `_authelia_scope_access(SERVICE_ID, DOMAIN)` is a -generic, reusable opt-in on top of that — call it right after *any* service -finishes being protected by Authelia, forward_auth gate or native OIDC -alike (it only cares about the domain, not the gating mechanism; see -`_gitea_offer_authelia_sso()` for the reference caller). Asks whether -access should stay universal or be scoped to specific usernames; if scoped, -creates a dedicated `-only` group, adds every listed username -to it (creating accounts on the fly via +**Internal vs. outside access — named, reusable groups, not one group per +service.** By default, any domain with an `access_control` rule at all is +"internal": reachable by every Authelia user (the existing catch-all +`*.${AUTHELIA_DOMAIN}` rule) — admins included automatically, since the +admin-bypass rule (below) always outranks it anyway. `services/authelia.sh`'s +`_authelia_scope_access(SERVICE_ID, DOMAIN)` is the generic, reusable opt-in +on top of that for "outside access" — call it right after *any* service +finishes being protected by Authelia, forward_auth gate or native OIDC alike +(it only cares about the domain, not the gating mechanism; see +`_gitea_offer_authelia_sso()` for the reference caller). Asks "Internal +(default) or Outside access", and if outside access, lets the admin pick an +*existing* named group (by number, so e.g. "customer1" can be attached to a +second, third, unrelated site later) or type a new one — `service_id` is +only the suggested default name, never forced. Creates the group if new +(adding every listed username to it, creating accounts on the fly via `_authelia_create_user_noninteractive()` for names that don't exist yet, -printing their temp password), and inserts two rules *above* the general -catch-all — allow that group on this domain, deny that group on every -other protected domain. Idempotent: reruns against an already-scoped -domain just report the existing group instead of duplicating rules. +printing their temp password) and inserts two rules *above* the general +catch-all but *below* the admin-bypass rule — allow that group on this +domain, deny that group on every other protected domain. The already-scoped +check is keyed to the (domain, group) pair, not the group name alone, so +reusing a group on a second site correctly adds that site's own rule instead +of a false "already scoped" no-op (a real bug in an earlier version of this +function, since fixed). + +Three more menu options round this out: **13** backfills the admin-bypass +rule (below) onto any apex domain missing it; **14** renames a group +everywhere it's referenced (rules + every member); **15** lists every +group's sites and members in one place; **16** is the reverse of the +scoping prompt's own member-picker — pick a group first, then toggle which +users are in it, for adding members without re-touching a site. + +**Admins always match first, on every domain — old sites and new.** +`_authelia_ensure_admin_bypass(config_file, domain)` inserts +`- domain: "*.${domain}" / subject: "group:admins" / policy: two_factor` as +literally the first rule under `rules:`, and every insertion point that +adds new rules (`add_authelia_domain`, `_authelia_scope_access`) inserts +*after* this block rather than at the literal top of `rules:`, so a later +scoping action can never accidentally outrank it. Without this, a group's +deny-elsewhere rule (above) would deny an admin who's ever added to that +group on every OTHER domain — this rule exists specifically so that can't +happen. `install_authelia()`/`add_authelia_domain()` bake it in for +anything created from here on; menu option 13 backfills it onto an +instance that predates the feature. Guard every cross-file call with `declare -F`, same convention as the OIDC helper above — a service can run standalone with authelia.sh never sourced. diff --git a/services/authelia.sh b/services/authelia.sh index 566f058..70bba65 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -250,10 +250,13 @@ install_authelia() { echo " 15) Show every group's sites and users (site groups + user groups overview)" echo " 16) Add/remove users from a group (pick the group, then toggle members —" echo " the reverse of option 4's per-user group toggle)" + echo " 17) Bulk-assign several users to one group at once (e.g. \"1 4 5\" ->" + echo " internal, then \"2 3 7\" -> a named group), with each user's" + echo " current access shown alongside their name" echo " 0) Leave as-is / exit" echo "" local EXISTING_CHOICE="" - prompt_text " Choice [1-16, 0 to exit]:" "0" EXISTING_CHOICE + prompt_text " Choice [1-17, 0 to exit]:" "0" EXISTING_CHOICE case "$EXISTING_CHOICE" in 1) add_authelia_domain @@ -318,6 +321,10 @@ install_authelia() { _authelia_manage_group_membership return 0 ;; + 17) + _authelia_bulk_assign_group + return 0 + ;; 0|*) echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)" return 0 @@ -1689,11 +1696,11 @@ _authelia_scope_access() { echo "" echo " Who should be able to reach $domain via Authelia?" - echo " 0) Native — your own users, no extra restriction (default — same" + echo " 0) Internal — your own users, no extra restriction (default — same" echo " access as everything else)" echo " 1) Outside access — a named group of specific users only" local scope_choice="" - prompt_text " Choice [0 for native, 1 for outside access]:" "0" scope_choice + prompt_text " Choice [0 for internal, 1 for outside access]:" "0" scope_choice [ "$scope_choice" = "1" ] || return 0 local -a existing_groups @@ -1936,7 +1943,7 @@ _authelia_report_groups() { local -a groups mapfile -t groups < <(_authelia_list_scoped_groups "$users_file") if [ "${#groups[@]}" -eq 0 ]; then - log_info "No outside-access groups exist yet — every site is native (open to any Authelia user, admins always included)." + log_info "No outside-access groups exist yet — every site is internal (open to any Authelia user, admins always included)." return 0 fi @@ -1995,7 +2002,7 @@ _authelia_manage_group_membership() { if [ "${#groups[@]}" -eq 0 ]; then log_info "No outside-access groups exist yet. A group is created the first time you" log_info "protect a site (option 10, or a service's own \"Add Sign in with Authelia\"" - log_info "offer) and choose \"Outside access\" instead of \"Native\" — name it there" + log_info "offer) and choose \"Outside access\" instead of \"Internal\" — name it there" log_info "(e.g. \"customer1\"), and it'll show up here afterward to manage its members." return 0 fi @@ -2075,6 +2082,129 @@ _authelia_manage_group_membership() { fi } +# Bulk version spanning BOTH axes at once — several users, one target group, +# in a single step (e.g. "1 4 5 6 -> internal", then "2 3 7 8 -> external1"), +# repeatable for as many user/group batches as needed in one menu visit. +# Complements the two single-axis tools above: option 6 (per-user menu) is +# one user, many groups to toggle; option 16 is one group, many users to +# toggle; this is many users, one group, picked together. "Internal" isn't +# a real group — picking it clears every outside-access group membership +# for the selected users, since internal access is the absence of a +# restricting group, not a group of its own. +_authelia_bulk_assign_group() { + local users_file="$DOCKER_DIR/authelia/config/users.yml" + [ -f "$users_file" ] || { log_warning "No users.yml found — install Authelia first."; return 1; } + + local KEEP_GOING="y" + while [[ "$KEEP_GOING" =~ ^[Yy]$ ]]; do + local -a all_users + mapfile -t all_users < <(_authelia_list_usernames "$users_file") + if [ "${#all_users[@]}" -eq 0 ]; then + log_info "No users exist yet — add one first (this menu's \"Add a new user\")." + return 0 + fi + + echo "" + echo " Existing users:" + local i start_end start end + for i in "${!all_users[@]}"; do + start_end="$(_authelia_user_line_range "$users_file" "${all_users[$i]}")" + start="${start_end% *}"; end="${start_end#* }" + echo " $((i + 1))) ${all_users[$i]} [$(_authelia_describe_user_access "$users_file" "$start" "$end")]" + done + echo "" + echo " Select one or more users by number (space-separated), or 0 to cancel." + local USEL="" + prompt_text " User number(s) [0]:" "0" USEL + if [ -z "$USEL" ] || [ "$USEL" = "0" ]; then + log_info "Cancelled." + return 0 + fi + + local -a usel_tokens targets=() + read -ra usel_tokens <<< "$USEL" + local tok + for tok in "${usel_tokens[@]}"; do + if [[ "$tok" =~ ^[0-9]+$ ]] && [ "$tok" -ge 1 ] && [ "$tok" -le "${#all_users[@]}" ]; then + targets+=("${all_users[$((tok - 1))]}") + else + log_warning "Skipping invalid selection: $tok" + fi + done + if [ "${#targets[@]}" -eq 0 ]; then + log_warning "No valid users selected." + prompt_yn " Try again? (y/n):" "n" KEEP_GOING + continue + fi + + local -a existing_groups + mapfile -t existing_groups < <(_authelia_list_scoped_groups "$users_file") + echo "" + echo " Assign ${#targets[@]} user(s) to:" + echo " 0) Internal — remove from every outside-access group" + local gi + for gi in "${!existing_groups[@]}"; do + echo " $((gi + 1))) ${existing_groups[$gi]%-only}" + done + echo " Or type a new group name to create one." + local GSEL="" + prompt_text " Group [0 for internal]:" "0" GSEL + + if [ -z "$GSEL" ] || [ "$GSEL" = "0" ]; then + local t t_start_end t_start t_end g + for t in "${targets[@]}"; do + t_start_end="$(_authelia_user_line_range "$users_file" "$t")" + t_start="${t_start_end% *}"; t_end="${t_start_end#* }" + for g in "${existing_groups[@]}"; do + sed -n "${t_start},${t_end}p" "$users_file" | grep -qF " - ${g}" \ + && _authelia_toggle_group "$users_file" "$t_start" "$t_end" "$g" "false" + done + log_success "${t} set to internal (removed from every outside-access group)" + done + else + local group="" + if [[ "$GSEL" =~ ^[0-9]+$ ]] && [ "$GSEL" -ge 1 ] && [ "$GSEL" -le "${#existing_groups[@]}" ]; then + group="${existing_groups[$((GSEL - 1))]}" + else + local clean_name + clean_name="$(echo "$GSEL" | tr -cs 'a-zA-Z0-9_-' '-' | sed 's/^-*//;s/-*$//')" + if [ -z "$clean_name" ]; then + log_warning "Invalid group name — nothing changed." + prompt_yn " Try again? (y/n):" "n" KEEP_GOING + continue + fi + group="${clean_name}-only" + local is_new="true" eg + for eg in "${existing_groups[@]}"; do [ "$eg" = "$group" ] && is_new="false"; done + if [ "$is_new" = "true" ]; then + log_warning "'$clean_name' isn't attached to any site yet — membership alone won't grant" + log_warning "access to anything until a site is scoped to it (site protection's" + log_warning "\"Outside access\" choice, or re-running a service's own SSO offer)." + fi + fi + local t t_start_end2 t_start2 t_end2 + for t in "${targets[@]}"; do + t_start_end2="$(_authelia_user_line_range "$users_file" "$t")" + t_start2="${t_start_end2% *}"; t_end2="${t_start_end2#* }" + _authelia_toggle_group "$users_file" "$t_start2" "$t_end2" "$group" "true" + log_success "Added ${t} to '${group%-only}'" + done + fi + + chown 1000:1000 "$users_file" 2>/dev/null || true + 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 "" + prompt_yn " Assign another batch (different users and/or a different group)? (y/n):" "n" KEEP_GOING + done +} + # Menu-driven, idempotent bulk version of _authelia_ensure_admin_bypass — # backfills the admin-bypass rule for every apex domain currently on this # instance in one pass. install_authelia and add_authelia_domain bake the @@ -2147,7 +2277,7 @@ _authelia_report_access_scope() { fi echo "" - echo " Native (universal — every protected domain, no outside-access group):" + echo " Internal (universal — every protected domain, no outside-access group):" local -a universal=() restricted=() local u start_end start end groups_in_range for u in "${all_users[@]}"; do @@ -2452,6 +2582,23 @@ _authelia_set_2fa_exempt() { chown 1000:1000 "$config_file" 2>/dev/null || true } +# One-line access summary for a user, e.g. "admin" / "internal" / +# "customer1, customer2" / "admin, customer1" — used by both +# edit_authelia_user()'s listing and _authelia_bulk_assign_group() so a +# user's current privileges are visible right where you're about to change +# them, not something you have to cross-check against option 15 first. +_authelia_describe_user_access() { + local users_file="$1" start="$2" end="$3" + local -a tags=() + sed -n "${start},${end}p" "$users_file" | grep -q '^ - admins$' && tags+=("admin") + local -a groups + mapfile -t groups < <(sed -n "${start},${end}p" "$users_file" | grep -oE '^ - [a-zA-Z0-9_-]+-only$' | sed 's/^ - //; s/-only$//') + tags+=("${groups[@]}") + [ "${#tags[@]}" -eq 0 ] && tags=("internal") + local IFS=", " + echo "${tags[*]}" +} + # Interactive: pick an existing user from users.yml, then act on them — # edit email/display name, force a password reset, reset their 2FA device, # toggle whether they need 2FA at all, or toggle admin group membership. @@ -2481,9 +2628,11 @@ edit_authelia_user() { echo "" echo " Existing users:" - local i + local i u_start_end u_start u_end for i in "${!USERNAMES[@]}"; do - echo " $((i + 1))) ${USERNAMES[$i]}" + u_start_end="$(_authelia_user_line_range "$USERS_FILE" "${USERNAMES[$i]}")" + u_start="${u_start_end% *}"; u_end="${u_start_end#* }" + echo " $((i + 1))) ${USERNAMES[$i]} [$(_authelia_describe_user_access "$USERS_FILE" "$u_start" "$u_end")]" done echo "" echo " Select one or more by number (space-separated, e.g. \"2 4\")," @@ -3200,14 +3349,20 @@ _authelia_add_oidc_client() { echo "" ;; 6) - echo " Beszel is PocketBase-based — its OAuth2 provider is a PocketBase admin" - echo " setting, not an API this script can write (checked against beszel.dev's" - echo " own docs). In the hub: Settings -> Auth providers -> OpenID Connect:" + echo " Beszel is PocketBase-based — its OAuth2 provider lives in PocketBase's" + echo " own admin panel underneath the hub, not the hub's own Settings page and" + echo " not an API this script can write (checked against beszel.dev's own docs):" + echo " 1) https:///_/#/settings -> toggle OFF \"Hide" + echo " collection create and edit controls\"" + echo " 2) Collections -> edit the \"users\" collection" + echo " 3) Options tab -> enable OAuth2 -> Add provider, fill in:" echo " Client ID: ${CLIENT_ID}" echo " Client Secret: ${CLIENT_SECRET_PLAIN}" echo " Auth URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/authorization" echo " Token URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/token" echo " User Info URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/userinfo" + echo " 4) Save, then toggle \"Hide collection create and edit controls\" back" + echo " ON — leaving it off is its own exposure once you're done" echo " Register your first Beszel account with a password BEFORE touching" echo " DISABLE_PASSWORD_AUTH/USER_CREATION in its .env — flipping those before" echo " a working login exists risks locking the hub's UI out entirely." diff --git a/services/beszel.sh b/services/beszel.sh index 1d8ddbf..50b487e 100644 --- a/services/beszel.sh +++ b/services/beszel.sh @@ -288,41 +288,73 @@ _beszel_offer_authelia_oidc() { fi echo "" - echo " In the hub: Settings -> Auth providers -> OpenID Connect:" + echo " This lives in PocketBase's own admin panel underneath the hub, not the" + echo " hub's own Settings page — checked against beszel.dev's OAuth guide" + echo " directly, exact steps:" + echo " 1) Go to https:///_/#/settings and toggle OFF" + echo " \"Hide collection create and edit controls\"" + echo " 2) Go to Collections, edit the \"users\" collection" + echo " 3) Options tab -> enable OAuth2 -> Add provider, fill in:" echo " Client ID: beszel" echo " Client Secret: $OIDC_CLIENT_SECRET_PLAIN" echo " Auth URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/authorization" echo " Token URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/token" echo " User Info URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/userinfo" + echo " 4) Save, then toggle \"Hide collection create and edit controls\" back ON" + echo " at /_/#/settings — leaving it off is its own exposure once you're done" echo "" log_warning "The Client Secret above is shown once — save it now." declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "beszel" "$APP_DOMAIN" - # DISABLE_PASSWORD_AUTH/USER_CREATION are real, documented env vars - # (beszel.dev's own OAuth guide) — but Beszel has no default account: - # the FIRST person to register becomes admin (see this file's own - # README section), and there's no signup-fallback like Mealie's - # OIDC_SIGNUP_ENABLED convenience wording to lean on if that hasn't - # happened yet. Off by default and gated behind an explicit warning — - # flipping this before a working login exists risks locking the hub's - # UI out with no account able to reach it at all. echo "" - local _disable_local="" - prompt_yn " Also disable Beszel's own password login, so Authelia is the only way in? Only do this AFTER you've registered a working account. (y/n):" "n" _disable_local - if [[ "$_disable_local" =~ ^[Yy]$ ]]; then - local _auto_register="" - prompt_yn " Auto-create Beszel accounts for new Authelia logins? (y/n):" "n" _auto_register - sed -i '/^DISABLE_PASSWORD_AUTH=/d; /^USER_CREATION=/d' "$dir/.env" - { - echo "DISABLE_PASSWORD_AUTH=true" - [[ "$_auto_register" =~ ^[Yy]$ ]] && echo "USER_CREATION=true" - } >> "$dir/.env" - chown "$ACTUAL_USER:$ACTUAL_USER" "$dir/.env" 2>/dev/null || true - ( cd "$dir" && docker compose up -d beszel ) \ - && log_success "Beszel's own password login is now disabled — Authelia is the only way in." \ - || log_warning "Restart failed — check: docker compose -f $dir/docker-compose.yml logs beszel" + log_info "Paste those values into Beszel's Settings -> Auth providers -> OpenID" + log_info "Connect page now, then log out and click through the Authelia login" + log_info "button to confirm it actually works — BEFORE going any further here." + echo "" + log_warning "The next step can disable Beszel's password login entirely. Confirmed" + log_warning "live: saying yes here before actually testing the button leaves NEITHER" + log_warning "login path working — the password form is gone, and the OAuth provider" + log_warning "was never actually finished on Beszel's side, so its button never" + log_warning "appears either. Re-run 'sudo ./setup.sh beszel' (choose update) any time" + log_warning "later to come back to this once you've verified the button works." +} + +# Split out from _beszel_offer_authelia_oidc so it can also be re-reached on +# its own via a later "update" rerun, once the admin has actually gone and +# tested the Authelia login button — see that function's own warning for +# why this can't be offered in the same breath as printing the paste-in +# values. DISABLE_PASSWORD_AUTH/USER_CREATION are real, documented env vars +# (beszel.dev's own OAuth guide). +_beszel_offer_disable_password_auth() { + local dir="$1" + [ -f "$dir/.env" ] || return 0 + grep -qF "client_id: 'beszel'" "$DOCKER_DIR/authelia/config/configuration.yml" 2>/dev/null || return 0 + grep -q '^DISABLE_PASSWORD_AUTH=true' "$dir/.env" 2>/dev/null && return 0 + + echo "" + local _tested="" + prompt_yn " Have you ALREADY logged into Beszel successfully using the Authelia button (not just pasted the values)? (y/n):" "n" _tested + if [[ ! "$_tested" =~ ^[Yy]$ ]]; then + log_info "Skipped. Test the Authelia login button first, then re-run 'sudo ./setup.sh beszel' (choose update) to come back to this." + return 0 fi + + local _disable_local="" + prompt_yn " Disable Beszel's own password login now, so Authelia is the only way in? (y/n):" "n" _disable_local + [[ "$_disable_local" =~ ^[Yy]$ ]] || return 0 + + local _auto_register="" + prompt_yn " Auto-create Beszel accounts for new Authelia logins? (y/n):" "n" _auto_register + sed -i '/^DISABLE_PASSWORD_AUTH=/d; /^USER_CREATION=/d' "$dir/.env" + { + echo "DISABLE_PASSWORD_AUTH=true" + [[ "$_auto_register" =~ ^[Yy]$ ]] && echo "USER_CREATION=true" + } >> "$dir/.env" + chown "$ACTUAL_USER:$ACTUAL_USER" "$dir/.env" 2>/dev/null || true + ( cd "$dir" && docker compose up -d beszel ) \ + && log_success "Beszel's own password login is now disabled — Authelia is the only way in." \ + || log_warning "Restart failed — check: docker compose -f $dir/docker-compose.yml logs beszel" } install_beszel() { @@ -361,6 +393,7 @@ install_beszel() { [[ "$FINISH_AGENT" =~ ^[Yy]$ ]] && _beszel_configure_agent "$DIR" "http://localhost:${_WP} (or its Caddy domain, once configured)" fi _beszel_offer_authelia_oidc "$DIR" + _beszel_offer_disable_password_auth "$DIR" return 0 ;; cancel) diff --git a/services/mealie.sh b/services/mealie.sh index 11c89ab..d64a91a 100644 --- a/services/mealie.sh +++ b/services/mealie.sh @@ -228,7 +228,10 @@ _mealie_offer_authelia_oidc() { log_info "Authelia SSO is already configured for Mealie (OIDC_* already set in $DIR/.env)." local RECONFIGURE="" prompt_yn " Reconfigure it (registers a fresh Authelia client + secret)? (y/n):" "n" RECONFIGURE - [[ "$RECONFIGURE" =~ ^[Yy]$ ]] || return 0 + if [[ ! "$RECONFIGURE" =~ ^[Yy]$ ]]; then + _mealie_offer_disable_password_login "$DIR" + return 0 + fi # ALLOW_PASSWORD_LOGIN isn't OIDC_-prefixed but is written by this # same step (see below) — strip it too so reconfiguring doesn't # leave a stale duplicate line if it's set again. @@ -251,27 +254,6 @@ _mealie_offer_authelia_oidc() { prompt_yn " Require two-factor for Mealie logins via Authelia too? (y/n):" "y" _2fa [[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor" - # Mealie's own docs document ALLOW_PASSWORD_LOGIN (hides the - # username+password fields entirely) and OIDC_AUTO_REDIRECT (skips the - # login page and jumps straight to the identity provider) as the real, - # supported way to make Authelia the only way in — confirmed against - # docs.mealie.io's OIDC and backend-config pages directly, not assumed. - # Off by default: this is a real access-control change (anyone who - # only has a local Mealie account, not an Authelia one, loses their - # login path), not just an additive convenience like the SSO button - # above. Recoverable any time by flipping these back and restarting — - # worth saying so, since a locked-out admin's first instinct otherwise - # is to worry the account itself is gone. - echo "" - local _disable_local="" - prompt_yn " Also disable Mealie's own username/password login, so Authelia is the only way in? (y/n):" "n" _disable_local - local _auto_redirect="" - if [[ "$_disable_local" =~ ^[Yy]$ ]]; then - log_warning "Anyone without an Authelia account (only a local Mealie one) will no longer be able to log in." - log_info "Reversible any time: set ALLOW_PASSWORD_LOGIN back to true in $DIR/.env and 'docker compose up -d'." - prompt_yn " Skip Mealie's login page entirely and jump straight to Authelia? (y/n):" "y" _auto_redirect - fi - if ! _authelia_provision_oidc_client "Mealie" "mealie" "$AUTH_POLICY" "y" "n" "${BASE_URL}/login"; then log_warning "Couldn't register Mealie as an OIDC client in Authelia — skipping SSO setup." return 0 @@ -289,15 +271,6 @@ OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET_PLAIN OIDC_CONFIGURATION_URL=$_discovery_url OIDC_PROVIDER_NAME=Authelia ENV - if [[ "$_disable_local" =~ ^[Yy]$ ]]; then - echo "ALLOW_PASSWORD_LOGIN=false" >> "$DIR/.env" - fi - if [[ "$_auto_redirect" =~ ^[Yy]$ ]]; then - { - echo "OIDC_AUTO_REDIRECT=true" - echo "OIDC_REMEMBER_ME=true" - } >> "$DIR/.env" - fi chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true # Mealie's OIDC redirect URI generation trusts X-Forwarded-* only from @@ -312,13 +285,59 @@ ENV 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 - local _success_msg="\"Sign in with Authelia\" added to Mealie — local login still works too." - [[ "$_disable_local" =~ ^[Yy]$ ]] && _success_msg="\"Sign in with Authelia\" added to Mealie — local username/password login is now disabled." (cd "$DIR" && docker compose up -d) \ - && log_success "$_success_msg" \ + && 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#*://}" + + echo "" + log_info "Test the \"Login with Authelia\" button on Mealie's own login page before" + log_info "disabling local login — re-run 'sudo ./setup.sh mealie' (choose update," + log_info "then \"Reconfigure? n\") once you've confirmed it works, and you'll be" + log_info "offered that as a separate step." +} + +# Split out from _mealie_offer_authelia_oidc so disabling local login is +# never offered in the same breath as first setting SSO up — confirmed +# live (on Beszel, same risk class) that saying yes before actually testing +# the Authelia button leaves both login paths broken at once. Only reached +# from a later "update" rerun once OIDC is already configured and the admin +# declines to reconfigure — i.e. after they've had a real chance to test it. +_mealie_offer_disable_password_login() { + local DIR="$1" + grep -q '^ALLOW_PASSWORD_LOGIN=false' "$DIR/.env" 2>/dev/null && return 0 + + echo "" + local _tested="" + prompt_yn " Have you ALREADY logged into Mealie successfully using the Authelia button (not just enabled it)? (y/n):" "n" _tested + if [[ ! "$_tested" =~ ^[Yy]$ ]]; then + log_info "Skipped. Test the Authelia login button first, then re-run 'sudo ./setup.sh mealie' (choose update) to come back to this." + return 0 + fi + + local _disable_local="" + prompt_yn " Also disable Mealie's own username/password login, so Authelia is the only way in? (y/n):" "n" _disable_local + [[ "$_disable_local" =~ ^[Yy]$ ]] || return 0 + + log_warning "Anyone without an Authelia account (only a local Mealie one) will no longer be able to log in." + log_info "Reversible any time: set ALLOW_PASSWORD_LOGIN back to true in $DIR/.env and 'docker compose up -d'." + local _auto_redirect="" + prompt_yn " Skip Mealie's login page entirely and jump straight to Authelia? (y/n):" "y" _auto_redirect + + sed -i '/^ALLOW_PASSWORD_LOGIN=/d; /^OIDC_AUTO_REDIRECT=/d; /^OIDC_REMEMBER_ME=/d' "$DIR/.env" + { + echo "ALLOW_PASSWORD_LOGIN=false" + if [[ "$_auto_redirect" =~ ^[Yy]$ ]]; then + echo "OIDC_AUTO_REDIRECT=true" + echo "OIDC_REMEMBER_ME=true" + fi + } >> "$DIR/.env" + chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true + + (cd "$DIR" && docker compose up -d) \ + && log_success "Local username/password login is now disabled — Authelia is the only way in." \ + || log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs" } install_mealie() {