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:
+178
-47
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user