From 753a8fdd43a1f59d62d7a5ff563873f71e49fd57 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 04:17:35 +0000 Subject: [PATCH 1/2] Give admins guaranteed access to every Authelia-protected site, old and new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "subject: group:admins" rule ahead of every domain's other rules, so admins always match first regardless of any per-service scoping (existing or future) on that domain — a group's deny-elsewhere rule can no longer catch an admin even if they're accidentally added to that group later. - install_authelia and add_authelia_domain bake the rule in at creation time - _authelia_scope_access retrofits it just-in-time before inserting its own deny-elsewhere rule, and anchors that rule below it instead of at the top - new menu option 13 (_authelia_ensure_admin_access_everywhere) backfills it across every domain on an install that predates this - remove_authelia_domain cleans the rule up too when a domain is removed, and its domain picker dedupes since two rules now share one domain string --- services/authelia.sh | 179 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 4 deletions(-) diff --git a/services/authelia.sh b/services/authelia.sh index 5afde51..f1ef80b 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -244,10 +244,12 @@ install_authelia() { echo " 11) Un-protect a site (undoes option 10 for one site)" echo " 12) Export/import user data (backup accounts + 2FA before a reinstall," echo " or restore a previous export)" + echo " 13) Make sure admins always have access to every site (old and new —" + echo " safe to re-run any time)" echo " 0) Leave as-is / exit" echo "" local EXISTING_CHOICE="" - prompt_text " Choice [1-12, 0 to exit]:" "0" EXISTING_CHOICE + prompt_text " Choice [1-13, 0 to exit]:" "0" EXISTING_CHOICE case "$EXISTING_CHOICE" in 1) add_authelia_domain @@ -296,6 +298,10 @@ install_authelia() { _authelia_export_import_users_menu return 0 ;; + 13) + _authelia_ensure_admin_access_everywhere + return 0 + ;; 0|*) echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)" return 0 @@ -451,6 +457,14 @@ authentication_backend: access_control: default_policy: deny rules: + # Admins always match first, on every domain this instance protects — this + # rule must stay above any per-service scoping rule (old or new) below it, + # or a group's "deny elsewhere" rule could accidentally catch an admin. + # add_authelia_domain and _authelia_scope_access both preserve this + # ordering automatically — see _authelia_ensure_admin_bypass. + - domain: "*.${AUTHELIA_DOMAIN}" + subject: "group:admins" + policy: two_factor - domain: "*.${AUTHELIA_DOMAIN}" policy: two_factor @@ -733,9 +747,16 @@ add_authelia_domain() { NEW_PORTAL_DOMAIN="${NEW_PORTAL_SUBDOMAIN}.${NEW_DOMAIN}" # ── access_control.rules: insert right after "rules:" ──────────────────── + # The admin-bypass rule goes first, this domain's own catch-all second — + # both inserted together in one shot so a fresh domain never has a window + # where the catch-all exists without the bypass above it. See + # _authelia_ensure_admin_bypass for why this ordering has to hold. awk -v domain="$NEW_DOMAIN" ' { print } /^ rules:$/ && !done { + print " - domain: \"*." domain "\"" + print " subject: \"group:admins\"" + print " policy: two_factor" print " - domain: \"*." domain "\"" print " policy: two_factor" done=1 @@ -861,7 +882,10 @@ remove_authelia_domain() { fi local -a apex_domains - mapfile -t apex_domains < <(grep -oE '^ - domain: "\*\.[^"]+"' "$CONFIG_FILE" | sed -E 's/^ - domain: "\*\.(.+)"$/\1/') + # Each apex domain now has two rules sharing this exact domain string — + # its admin-bypass rule and its plain catch-all (see + # _authelia_ensure_admin_bypass) — so dedupe or it'd list every domain twice. + mapfile -t apex_domains < <(grep -oE '^ - domain: "\*\.[^"]+"' "$CONFIG_FILE" | sed -E 's/^ - domain: "\*\.(.+)"$/\1/' | awk '!seen[$0]++') echo "" if [ "${#apex_domains[@]}" -eq 0 ]; then @@ -913,6 +937,23 @@ remove_authelia_domain() { prompt_yn " Continue? (y/n):" "n" CONFIRM_RM [[ "$CONFIRM_RM" =~ ^[Yy]$ ]] || { log_info "Cancelled — nothing changed."; return 0; } + # ── access_control.rules: remove this domain's admin-bypass rule first ──── + # (3 lines: domain/subject/policy — see _authelia_ensure_admin_bypass) — + # has to run before the plain catch-all removal below, which only knows + # how to strip a 2-line domain/policy pair and would otherwise leave this + # rule's own "policy:" line orphaned, the exact corruption class this + # instance was repaired from once already. + awk -v domain="$RM_DOMAIN" ' + BEGIN { skip=0 } + skip > 0 { skip--; next } + $0 == " - domain: \"*." domain "\"" { + getline nxt + if (nxt == " subject: \"group:admins\"") { skip=1; next } + print; print nxt; next + } + { print } + ' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" + # ── access_control.rules: remove the "- domain: "*.X"" + "policy: ..." pair ── awk -v domain="$RM_DOMAIN" ' BEGIN { skip=0 } @@ -1543,6 +1584,71 @@ _authelia_create_user_noninteractive() { # domain that's already scoped just report the existing group instead of # duplicating rules. # +# Idempotent: makes sure a "subject: group:admins" rule exists for $domain's +# access_control.rules, above every other rule for that domain, then returns +# the 1-indexed line number its "policy:" line ended up on (via ADMIN_BYPASS_LINE, +# not stdout — awk already uses stdout for the rewritten file). Callers that +# insert a NEW rule for this domain (_authelia_scope_access's deny-elsewhere +# rule in particular) must insert it AFTER that line, never at the literal +# top of "rules:" — Authelia takes the first matching rule, so a deny rule +# landing above the admin-bypass rule would catch an admin who's also (by +# mistake, or by some future feature) a member of the group being denied. +# install_authelia and add_authelia_domain both bake this rule in directly +# at creation time instead of calling this — it's for retrofitting an apex +# domain that predates this (see _authelia_ensure_admin_access_everywhere, +# the menu-driven bulk version of this for an existing install) and for +# _authelia_scope_access's own just-in-time safety net. +_authelia_ensure_admin_bypass() { + local config_file="$1" domain="$2" + ADMIN_BYPASS_LINE="" + ADMIN_BYPASS_ADDED="false" + + local result + result="$(awk -v domain="$domain" ' + { lines[NR] = $0 } + END { + for (i = 1; i < NR; i++) { + if (lines[i] == " - domain: \"*." domain "\"" && lines[i+1] == " subject: \"group:admins\"") { + print i + 2 + exit + } + } + } + ' "$config_file")" + + if [ -n "$result" ]; then + ADMIN_BYPASS_LINE="$result" + return 0 + fi + + ADMIN_BYPASS_ADDED="true" + awk -v domain="$domain" ' + { print } + /^ rules:$/ && !done { + print " - domain: \"*." domain "\"" + print " subject: \"group:admins\"" + print " policy: two_factor" + done=1 + } + ' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file" + chown 1000:1000 "$config_file" 2>/dev/null || true + + # The block we just inserted is always lines 2-4 counting from " rules:" + # (which awk just placed the block right after) — but simplest and least + # fragile is to just re-run the same lookup now that it exists. + ADMIN_BYPASS_LINE="$(awk -v domain="$domain" ' + { lines[NR] = $0 } + END { + for (i = 1; i < NR; i++) { + if (lines[i] == " - domain: \"*." domain "\"" && lines[i+1] == " subject: \"group:admins\"") { + print i + 2 + exit + } + } + } + ' "$config_file")" +} + # Args: SERVICE_ID DOMAIN _authelia_scope_access() { local service_id="$1" domain="$2" @@ -1624,6 +1730,16 @@ _authelia_scope_access() { # 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")" + + # ...but BELOW the admin-bypass rule for this apex — otherwise the + # deny-elsewhere rule below would outrank it, and an admin who ever ends + # up in this group (by mistake, or a future feature) would be locked out + # of every other domain on the instance. Ensures the bypass rule exists + # first (retrofits it if this instance predates the feature), then + # inserts right after it rather than at the literal top of "rules:". + _authelia_ensure_admin_bypass "$config_file" "$authelia_domain" + local anchor_line="$ADMIN_BYPASS_LINE" + local scope_rules=" - domain: \"${domain}\" subject: \"group:${group}\" policy: two_factor @@ -1631,9 +1747,10 @@ _authelia_scope_access() { subject: \"group:${group}\" policy: deny" - awk -v block="$scope_rules" ' - /^ rules:$/ && !done { print; print block; done=1; next } + awk -v block="$scope_rules" -v anchor="$anchor_line" ' { print } + anchor != "" && NR == anchor + 0 { print block } + anchor == "" && /^ rules:$/ && !done { print block; done=1 } ' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file" chown 1000:1000 "$config_file" 2>/dev/null || true @@ -1646,6 +1763,60 @@ _authelia_scope_access() { fi } +# 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 +# rule in automatically for anything created from here on, and +# _authelia_scope_access retrofits it just-in-time for whichever domain it's +# scoping — this is for catching every OTHER domain an install already had +# before this feature existed (or just double-checking one that's fine). +# Safe to re-run any time; only touches domains actually missing the rule. +_authelia_ensure_admin_access_everywhere() { + local config_file="$DOCKER_DIR/authelia/config/configuration.yml" + [ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; } + + local -a apex_domains + mapfile -t apex_domains < <(grep -oE '^ - domain: "\*\.[^"]+"' "$config_file" | sed -E 's/^ - domain: "\*\.(.+)"$/\1/' | awk '!seen[$0]++') + + if [ "${#apex_domains[@]}" -eq 0 ]; then + log_info "No apex domains found on this instance." + return 0 + fi + + echo "" + echo " Checking the admin-bypass rule for every domain on this instance:" + local d any_added="false" + for d in "${apex_domains[@]}"; do + _authelia_ensure_admin_bypass "$config_file" "$d" + if [ "$ADMIN_BYPASS_ADDED" = "true" ]; then + echo " ${d} — added" + any_added="true" + else + echo " ${d} — already present" + fi + done + + if [ "$any_added" = "false" ]; then + log_success "Every domain already had it — nothing to change." + return 0 + fi + + log_success "Members of the 'admins' group now always have access to every domain" + log_success "listed above, regardless of any per-service scoping already in place —" + log_success "or added later, since _authelia_scope_access checks for this automatically" + log_success "from now on." + log_warning "Make sure your admin account(s) are actually in the 'admins' group in" + log_warning "config/users.yml — the default admin created at install time already is." + + 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 +} + # Reporting/management: lists which users have "universal" access (every # protected domain — anyone not locked into a "-only" group) versus # which are scoped to specific services, then offers to promote a scoped From 8aea5055410db243c99c22f7ec338bb79412b1f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 04:28:32 +0000 Subject: [PATCH 2/2] Generalize site scoping into reusable, named "outside access" groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _authelia_scope_access previously derived a throwaway "-only" group every time it ran, so scoping two different sites to the same set of people meant either duplicating membership by hand or hitting a false "already scoped" early-return that silently skipped adding the second site's own rule. Now it offers existing groups by number (any site can join one), lets a new name be typed freely (e.g. "customer1"), and the already-scoped check is keyed to the (domain, group) pair instead of the group name alone. Reframes the access question as native (default, unrestricted) vs. outside access (a named group) per the AD-style users/groups mental model, and adds menu option 14 to rename an existing group everywhere it's referenced (access_control.rules subjects + every member's users.yml entry). The "-only" suffix stays internal only — every other function that already keys off it (reporting, per-user group toggle, unprotect cleanup) is untouched. --- services/authelia.sh | 225 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 178 insertions(+), 47 deletions(-) diff --git a/services/authelia.sh b/services/authelia.sh index f1ef80b..f4bed01 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -246,10 +246,11 @@ install_authelia() { echo " or restore a previous export)" echo " 13) Make sure admins always have access to every site (old and new —" echo " safe to re-run any time)" + echo " 14) Rename an outside-access group (e.g. \"customer1\" -> \"acme-corp\")" echo " 0) Leave as-is / exit" echo "" local EXISTING_CHOICE="" - prompt_text " Choice [1-13, 0 to exit]:" "0" EXISTING_CHOICE + prompt_text " Choice [1-14, 0 to exit]:" "0" EXISTING_CHOICE case "$EXISTING_CHOICE" in 1) add_authelia_domain @@ -302,6 +303,10 @@ install_authelia() { _authelia_ensure_admin_access_everywhere return 0 ;; + 14) + _authelia_rename_group + return 0 + ;; 0|*) echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)" return 0 @@ -1650,6 +1655,19 @@ _authelia_ensure_admin_bypass() { } # Args: SERVICE_ID DOMAIN +# +# service_id is only ever used as the SUGGESTED group name when creating a +# brand-new group — the actual group is whatever the user picks or types +# below, so the same group (e.g. "customer1-only") can be attached to +# several different sites over time instead of getting a fresh +# "-only" group every call. The "-only" suffix itself is kept +# internally (never shown to the user, who just sees "customer1") because +# it's load-bearing elsewhere: _authelia_list_scoped_groups, +# _authelia_report_access_scope, edit_authelia_user's per-user group +# toggle, and _authelia_unprotect_site's cleanup all already key off that +# exact suffix pattern to find "this is a site-scoping group, not some +# other group a user happens to be in" — dropping it would mean touching +# all four of those instead of just this one function. _authelia_scope_access() { local service_id="$1" domain="$2" local authelia_dir="$DOCKER_DIR/authelia" @@ -1658,54 +1676,100 @@ _authelia_scope_access() { [ -f "$config_file" ] || return 0 - local group="${service_id}-only" + echo "" + echo " Who should be able to reach $domain via Authelia?" + echo " 0) Native — 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 + [ "$scope_choice" = "1" ] || return 0 - if grep -qF "subject: \"group:${group}\"" "$config_file" 2>/dev/null; then - log_info "Access to $domain is already scoped to group '$group'." + local -a existing_groups + mapfile -t existing_groups < <(_authelia_list_scoped_groups "$users_file") + local group="" is_new_group="true" i + if [ "${#existing_groups[@]}" -gt 0 ]; then + echo " Existing outside-access groups:" + for i in "${!existing_groups[@]}"; do + echo " $((i + 1))) ${existing_groups[$i]%-only}" + done + echo " Pick a number to add $domain to one of these, or type a new group" + echo " name (e.g. \"customer1\") to create one." + else + echo " No outside-access groups exist yet — type a name to create one" + echo " (e.g. \"customer1\")." + fi + local group_choice="" + prompt_text " Group:" "${service_id}" group_choice + if [ -z "$group_choice" ]; then + log_warning "No group entered — leaving $domain open to all Authelia users." + return 0 + fi + if [[ "$group_choice" =~ ^[0-9]+$ ]] && [ "$group_choice" -ge 1 ] && [ "$group_choice" -le "${#existing_groups[@]}" ]; then + group="${existing_groups[$((group_choice - 1))]}" + is_new_group="false" + else + local clean_name + clean_name="$(echo "$group_choice" | tr -cs 'a-zA-Z0-9_-' '-' | sed 's/^-*//;s/-*$//')" + if [ -z "$clean_name" ]; then + log_warning "Invalid group name — leaving $domain open to all Authelia users." + return 0 + fi + group="${clean_name}-only" + for i in "${existing_groups[@]}"; do + [ "$i" = "$group" ] && is_new_group="false" + done + fi + + if grep -A1 -F " - domain: \"${domain}\"" "$config_file" 2>/dev/null | grep -qF " subject: \"group:${group}\""; then + log_info "$domain is already scoped to group '${group%-only}'." 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) Specific users only" - echo " 0) Any Authelia user (default — same access as everything else)" - local scope_choice="" - prompt_text " Choice [1, or 0 for any user]:" "0" scope_choice - [ "$scope_choice" = "1" ] || return 0 - - local -a existing_users - mapfile -t existing_users < <(_authelia_list_usernames "$users_file") - local i - if [ "${#existing_users[@]}" -gt 0 ]; then - echo " Existing Authelia users:" - for i in "${!existing_users[@]}"; do - echo " $((i + 1))) ${existing_users[$i]}" - done - echo " Pick by number (space-separated), and/or type new usernames directly" - echo " to create them — mix freely, e.g. \"1 3 newperson\"." - else - echo " No existing Authelia users yet — type usernames below to create them fresh." - fi - echo " Anyone typed (not picked by number) who doesn't already have an" - echo " Authelia account gets one created — you'll get their temporary" - echo " password to hand over." - local raw_users="" - prompt_text " Usernames/numbers:" "" raw_users - local -a raw_tokens usernames - read -ra raw_tokens <<< "$raw_users" - if [ "${#raw_tokens[@]}" -eq 0 ]; then - log_warning "No usernames entered — leaving $domain open to all Authelia users." - return 0 - fi - local t - for t in "${raw_tokens[@]}"; do - if [[ "$t" =~ ^[0-9]+$ ]] && [ "$t" -ge 1 ] && [ "$t" -le "${#existing_users[@]}" ]; then - usernames+=("${existing_users[$((t - 1))]}") - else - usernames+=("$t") + local -a usernames + if [ "$is_new_group" = "false" ]; then + log_info "Reusing existing group '${group%-only}' — its current members already have access." + local add_more="" + prompt_yn " Add more users to '${group%-only}' now? (y/n):" "n" add_more + if [[ ! "$add_more" =~ ^[Yy]$ ]]; then + usernames=() fi - done + fi + + if [ "$is_new_group" = "true" ] || [[ "${add_more:-}" =~ ^[Yy]$ ]]; then + local -a existing_users + mapfile -t existing_users < <(_authelia_list_usernames "$users_file") + if [ "${#existing_users[@]}" -gt 0 ]; then + echo " Existing Authelia users:" + for i in "${!existing_users[@]}"; do + echo " $((i + 1))) ${existing_users[$i]}" + done + echo " Pick by number (space-separated), and/or type new usernames directly" + echo " to create them — mix freely, e.g. \"1 3 newperson\"." + else + echo " No existing Authelia users yet — type usernames below to create them fresh." + fi + echo " Anyone typed (not picked by number) who doesn't already have an" + echo " Authelia account gets one created — you'll get their temporary" + echo " password to hand over." + local raw_users="" + prompt_text " Usernames/numbers:" "" raw_users + local -a raw_tokens + read -ra raw_tokens <<< "$raw_users" + if [ "${#raw_tokens[@]}" -eq 0 ] && [ "$is_new_group" = "true" ]; then + log_warning "No usernames entered — leaving $domain open to all Authelia users." + return 0 + fi + local t + for t in "${raw_tokens[@]}"; do + if [[ "$t" =~ ^[0-9]+$ ]] && [ "$t" -ge 1 ] && [ "$t" -le "${#existing_users[@]}" ]; then + usernames+=("${existing_users[$((t - 1))]}") + else + usernames+=("$t") + fi + done + fi local u start_end start end for u in "${usernames[@]}"; do @@ -1715,7 +1779,7 @@ _authelia_scope_access() { 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'" + log_success "Added '$u' to group '${group%-only}'" else local email_default="${u}@${SITE_DOMAIN:-example.com}" if _authelia_create_user_noninteractive "$u" "$u" "$email_default" "$group"; then @@ -1758,7 +1822,74 @@ _authelia_scope_access() { 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_success "Authelia restarted — $domain is now restricted to group '${group%-only}'." \ + || log_warning "Restart failed — check: docker compose logs authelia" + fi +} + +# Renames an existing outside-access group everywhere it appears — every +# "subject: group:" line in configuration.yml's access_control.rules, +# and every member's "- " entry under their own groups: list in +# users.yml. A plain find/replace on the "-only"-suffixed internal name; +# the display name typed at the prompt (what _authelia_scope_access shows +# without the suffix) is what the user actually renames. +_authelia_rename_group() { + local users_file="$DOCKER_DIR/authelia/config/users.yml" + local config_file="$DOCKER_DIR/authelia/config/configuration.yml" + [ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; } + + local -a existing_groups + mapfile -t existing_groups < <(_authelia_list_scoped_groups "$users_file") + if [ "${#existing_groups[@]}" -eq 0 ]; then + log_info "No outside-access groups exist yet." + return 0 + fi + + echo "" + echo " Outside-access groups:" + local i + for i in "${!existing_groups[@]}"; do + echo " $((i + 1))) ${existing_groups[$i]%-only}" + done + echo " 0) Cancel" + local choice="" + prompt_text " Which group to rename? [0]:" "0" choice + if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#existing_groups[@]}" ]; then + log_info "Cancelled — nothing changed." + return 0 + fi + local old_group="${existing_groups[$((choice - 1))]}" + + local new_name="" + prompt_text " New name for '${old_group%-only}':" "" new_name + local clean_name + clean_name="$(echo "$new_name" | tr -cs 'a-zA-Z0-9_-' '-' | sed 's/^-*//;s/-*$//')" + if [ -z "$clean_name" ]; then + log_warning "No name entered — nothing changed." + return 0 + fi + local new_group="${clean_name}-only" + if [ "$new_group" = "$old_group" ]; then + log_info "Same name — nothing changed." + return 0 + fi + for i in "${existing_groups[@]}"; do + if [ "$i" = "$new_group" ]; then + log_warning "A group named '${clean_name}' already exists — pick a different name, or use that group directly instead of renaming into it." + return 1 + fi + done + + sed -i "s/subject: \"group:${old_group}\"/subject: \"group:${new_group}\"/g" "$config_file" + sed -i "s/^ - ${old_group}\$/ - ${new_group}/g" "$users_file" + chown 1000:1000 "$config_file" "$users_file" 2>/dev/null || true + log_success "Renamed '${old_group%-only}' to '${clean_name}' — updated every access rule and member using it." + + 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 } @@ -1835,7 +1966,7 @@ _authelia_report_access_scope() { fi echo "" - echo " Universal access (every protected domain):" + echo " Native (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 @@ -1852,7 +1983,7 @@ _authelia_report_access_scope() { [ "${#universal[@]}" -eq 0 ] && echo " (none)" echo "" - echo " Scoped to specific services only:" + echo " Outside access (limited to a named group):" if [ "${#restricted[@]}" -eq 0 ]; then echo " (none)" else