Generalize site scoping into reusable, named "outside access" groups

_authelia_scope_access previously derived a throwaway "<service>-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.
This commit is contained in:
Claude
2026-08-24 04:28:32 +00:00
parent 753a8fdd43
commit 8aea505541
+178 -47
View File
@@ -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
# "<service_id>-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:<old>" line in configuration.yml's access_control.rules,
# and every member's "- <old>" 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