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