Add "Edit an existing user" to authelia.sh
New menu option lists existing users by number; picking one opens a submenu to edit email/display name, force a password reset, reset a 2FA device (authelia storage user totp delete), toggle a one_factor exemption for that user via a subject-scoped access_control rule, and promote/demote admin group membership. All the YAML-editing helpers (line-range lookup, scoped field/group edits, and the access_control rule insertion/removal used by the 2FA exemption toggle) are line-range-scoped to the target user only, and were verified against single- and multi-domain/multi-user fixtures before wiring them into the interactive flow — a bad edit to access_control here would break every protected domain, not just one user's account.
This commit is contained in:
+270
-5
@@ -226,14 +226,16 @@ install_authelia() {
|
||||
echo " 1) Add another protected domain to this instance (non-destructive —"
|
||||
echo " one Authelia+Redis, multiple independent apex domains/logins)"
|
||||
echo " 2) Add a new user (creates a users.yml entry + password hash)"
|
||||
echo " 3) Register an app to log in VIA Authelia (OIDC/SSO — e.g. ActualBudget,"
|
||||
echo " 3) Edit an existing user (email, password reset, 2FA reset/exempt,"
|
||||
echo " promote/demote admin)"
|
||||
echo " 4) Register an app to log in VIA Authelia (OIDC/SSO — e.g. ActualBudget,"
|
||||
echo " Vaultwarden, or any other app with its own \"Enable OpenID\" setting)"
|
||||
echo " 4) Reconfigure from scratch (regenerates secrets/users — breaks"
|
||||
echo " 5) Reconfigure from scratch (regenerates secrets/users — breaks"
|
||||
echo " existing sessions for every domain already on this instance)"
|
||||
echo " 5) Leave as-is"
|
||||
echo " 6) Leave as-is"
|
||||
echo ""
|
||||
local EXISTING_CHOICE=""
|
||||
prompt_text " Choice [1/2/3/4/5]:" "5" EXISTING_CHOICE
|
||||
prompt_text " Choice [1/2/3/4/5/6]:" "6" EXISTING_CHOICE
|
||||
case "$EXISTING_CHOICE" in
|
||||
1)
|
||||
add_authelia_domain
|
||||
@@ -244,10 +246,14 @@ install_authelia() {
|
||||
return 0
|
||||
;;
|
||||
3)
|
||||
_authelia_add_oidc_client
|
||||
edit_authelia_user
|
||||
return 0
|
||||
;;
|
||||
4)
|
||||
_authelia_add_oidc_client
|
||||
return 0
|
||||
;;
|
||||
5)
|
||||
: # fall through to the full reinstall flow below
|
||||
;;
|
||||
*)
|
||||
@@ -842,6 +848,265 @@ ${GROUPS_BLOCK}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── edit_authelia_user() helpers ──────────────────────────────────────────────
|
||||
# All of these operate on a caller-supplied line range or file, never scan the
|
||||
# whole file themselves, so an edit to one user's block can't bleed into a
|
||||
# neighboring user (or, for the 2FA-exempt helpers, one user's exemption rule
|
||||
# can't be mistaken for another's — verified against multi-user/multi-domain
|
||||
# fixtures before this shipped, since a bad access_control edit here would
|
||||
# break every protected domain on the instance, not just this one user).
|
||||
|
||||
_authelia_list_usernames() {
|
||||
local users_file="$1"
|
||||
awk '/^users:$/{f=1; next} f && /^ [A-Za-z0-9_-]+:$/{gsub(/^ /,""); gsub(/:$/,""); print}' "$users_file"
|
||||
}
|
||||
|
||||
# Prints "<start_line> <end_line>" (1-indexed, inclusive) spanning just the
|
||||
# given user's block in users.yml.
|
||||
_authelia_user_line_range() {
|
||||
local users_file="$1" username="$2"
|
||||
awk -v user="$username" '
|
||||
BEGIN{start=0; end=0}
|
||||
/^ [A-Za-z0-9_-]+:$/ {
|
||||
if (start>0 && end==0) { end=NR-1 }
|
||||
if ($0 ~ "^ "user":$") { start=NR }
|
||||
}
|
||||
END {
|
||||
if (start>0 && end==0) { end=NR }
|
||||
print start, end
|
||||
}
|
||||
' "$users_file"
|
||||
}
|
||||
|
||||
# Replaces the first " <field>: ..." line found within [start,end] with
|
||||
# "newline" verbatim (caller supplies correct quoting for that field).
|
||||
_authelia_set_user_field() {
|
||||
local users_file="$1" start="$2" end="$3" field="$4" newline="$5"
|
||||
awk -v s="$start" -v e="$end" -v field="$field" -v newline="$newline" '
|
||||
NR>=s && NR<=e && $0 ~ "^ "field":" { print newline; next }
|
||||
{ print }
|
||||
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
|
||||
}
|
||||
|
||||
# enable=true adds "- admins" under this user's groups: (no-op if already
|
||||
# present); enable=false removes it. Scoped to [start,end] so it can't touch
|
||||
# another user's groups list.
|
||||
_authelia_toggle_admin() {
|
||||
local users_file="$1" start="$2" end="$3" enable="$4"
|
||||
if [ "$enable" = "true" ]; then
|
||||
if ! sed -n "${start},${end}p" "$users_file" | grep -q '^ - admins$'; then
|
||||
awk -v s="$start" -v e="$end" '
|
||||
{ print }
|
||||
NR>=s && NR<=e && /^ groups:$/ { print " - admins" }
|
||||
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
|
||||
fi
|
||||
else
|
||||
awk -v s="$start" -v e="$end" '
|
||||
NR>=s && NR<=e && /^ - admins$/ { next }
|
||||
{ print }
|
||||
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# action="exempt": inserts a "policy: one_factor / subject: user:<name>" rule
|
||||
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
|
||||
# configuration.yml (handles multi-domain instances from add_authelia_domain
|
||||
# automatically). action="restore": removes only this user's own such rules,
|
||||
# leaving any other user's exemptions and the catch-all rules untouched.
|
||||
# Caller is responsible for the idempotency check (only offer "exempt" in the
|
||||
# menu when not already exempt, and vice versa) — this helper doesn't dedupe.
|
||||
_authelia_set_2fa_exempt() {
|
||||
local config_file="$1" username="$2" action="$3"
|
||||
if [ "$action" = "exempt" ]; then
|
||||
awk -v user="$username" '
|
||||
{ lines[NR]=$0 }
|
||||
END {
|
||||
for (i=1; i<=NR; i++) {
|
||||
if (lines[i] ~ /^ - domain:/ && lines[i+1] ~ /policy: two_factor/) {
|
||||
domain = lines[i]
|
||||
sub(/^ - domain: /, "", domain)
|
||||
print " - domain: " domain
|
||||
print " policy: one_factor"
|
||||
print " subject: \"user:" user "\""
|
||||
}
|
||||
print lines[i]
|
||||
}
|
||||
}
|
||||
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
else
|
||||
awk -v user="$username" '
|
||||
{ lines[NR]=$0 }
|
||||
END {
|
||||
for (i=1; i<=NR; i++) {
|
||||
if (lines[i] ~ /^ - domain:/ && lines[i+1] ~ /policy: one_factor/ && lines[i+2] ~ ("subject: \"user:" user "\"")) {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
print lines[i]
|
||||
}
|
||||
}
|
||||
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
fi
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# 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.
|
||||
# Loops so multiple actions can be applied to the same user in one pass.
|
||||
edit_authelia_user() {
|
||||
local AUTHELIA_DIR="$DOCKER_DIR/authelia"
|
||||
local USERS_FILE="$AUTHELIA_DIR/config/users.yml"
|
||||
local CONFIG_FILE="$AUTHELIA_DIR/config/configuration.yml"
|
||||
|
||||
if [ ! -f "$USERS_FILE" ]; then
|
||||
log_warning "No users.yml found at $USERS_FILE — install Authelia first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a USERNAMES
|
||||
mapfile -t USERNAMES < <(_authelia_list_usernames "$USERS_FILE")
|
||||
if [ "${#USERNAMES[@]}" -eq 0 ]; then
|
||||
log_warning "No users found in $USERS_FILE."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Existing users:"
|
||||
local i=1 u
|
||||
for u in "${USERNAMES[@]}"; do
|
||||
echo " $i) $u"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo ""
|
||||
local SEL=""
|
||||
prompt_text " Select a user by number (blank to cancel):" "" SEL
|
||||
if [ -z "$SEL" ] || ! [[ "$SEL" =~ ^[0-9]+$ ]] || [ "$SEL" -lt 1 ] || [ "$SEL" -gt "${#USERNAMES[@]}" ]; then
|
||||
log_info "Cancelled."
|
||||
return 0
|
||||
fi
|
||||
local TARGET="${USERNAMES[$((SEL - 1))]}"
|
||||
|
||||
local CONTINUE="y"
|
||||
while [[ "$CONTINUE" =~ ^[Yy]$ ]]; do
|
||||
local RANGE START END
|
||||
RANGE="$(_authelia_user_line_range "$USERS_FILE" "$TARGET")"
|
||||
START="${RANGE% *}"; END="${RANGE#* }"
|
||||
|
||||
local IS_ADMIN="no"
|
||||
sed -n "${START},${END}p" "$USERS_FILE" | grep -q '^ - admins$' && IS_ADMIN="yes"
|
||||
local IS_EXEMPT="no"
|
||||
[ -f "$CONFIG_FILE" ] && grep -qF "subject: \"user:${TARGET}\"" "$CONFIG_FILE" && IS_EXEMPT="yes"
|
||||
|
||||
echo ""
|
||||
echo " Editing user: $TARGET (admin: $IS_ADMIN, 2FA-exempt: $IS_EXEMPT)"
|
||||
echo " 1) Edit email / display name"
|
||||
echo " 2) Reset password"
|
||||
echo " 3) Reset 2FA device (they register a new one on next login)"
|
||||
if [ "$IS_EXEMPT" = "yes" ]; then
|
||||
echo " 4) Restore the 2FA requirement for this user"
|
||||
else
|
||||
echo " 4) Exempt this user from 2FA (one_factor only — weakens their account)"
|
||||
fi
|
||||
if [ "$IS_ADMIN" = "yes" ]; then
|
||||
echo " 5) Demote from admin"
|
||||
else
|
||||
echo " 5) Promote to admin"
|
||||
fi
|
||||
echo " 6) Done with this user"
|
||||
echo ""
|
||||
local ACTION=""
|
||||
prompt_text " Choice [1-6]:" "6" ACTION
|
||||
|
||||
case "$ACTION" in
|
||||
1)
|
||||
local CUR_EMAIL CUR_DISPLAY NEW_EMAIL NEW_DISPLAY
|
||||
CUR_EMAIL="$(sed -n "${START},${END}p" "$USERS_FILE" | grep '^ email:' | sed 's/^ email: *//')"
|
||||
CUR_DISPLAY="$(sed -n "${START},${END}p" "$USERS_FILE" | grep '^ displayname:' | sed 's/^ displayname: *//; s/^"//; s/"$//')"
|
||||
prompt_text " New email [$CUR_EMAIL]:" "$CUR_EMAIL" NEW_EMAIL
|
||||
prompt_text " New display name [$CUR_DISPLAY]:" "$CUR_DISPLAY" NEW_DISPLAY
|
||||
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "email" " email: ${NEW_EMAIL}"
|
||||
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "displayname" " displayname: \"${NEW_DISPLAY}\""
|
||||
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
|
||||
log_success "Updated $TARGET's email/display name."
|
||||
;;
|
||||
2)
|
||||
log_info "Generating a new temporary password + hash..."
|
||||
local NEW_TEMP_PASS NEW_HASH
|
||||
NEW_TEMP_PASS="$(_authelia_gen_temp_password)"
|
||||
NEW_HASH=$(docker run --rm authelia/authelia:4.39.20 \
|
||||
authelia crypto hash generate argon2 --password "$NEW_TEMP_PASS" 2>/dev/null \
|
||||
| grep -oP '(?<=Digest: ).*')
|
||||
if [ -z "$NEW_HASH" ]; then
|
||||
log_warning "Couldn't generate the password hash automatically — nothing changed. Try again."
|
||||
else
|
||||
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "password" " password: \"${NEW_HASH}\""
|
||||
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
|
||||
log_success "Password reset for $TARGET."
|
||||
echo " New password: ${NEW_TEMP_PASS}"
|
||||
echo " Give this to them directly — shown once, not stored in plaintext anywhere."
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
if docker ps --format '{{.Names}}' | grep -q '^authelia$'; then
|
||||
if docker exec authelia authelia storage user totp delete "$TARGET" --config /config/configuration.yml 2>/dev/null; then
|
||||
log_success "TOTP device reset for $TARGET — they'll register a new one on next login."
|
||||
else
|
||||
log_warning "No TOTP device found for $TARGET (or the delete failed) — check: docker compose logs authelia"
|
||||
fi
|
||||
echo " WebAuthn devices (if any) aren't covered by this option — reset those manually with:"
|
||||
echo " docker exec authelia authelia storage user webauthn delete --username $TARGET --config /config/configuration.yml"
|
||||
else
|
||||
log_warning "Authelia isn't running — start it first: cd $AUTHELIA_DIR && docker compose up -d"
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
if [ "$IS_EXEMPT" = "yes" ]; then
|
||||
_authelia_set_2fa_exempt "$CONFIG_FILE" "$TARGET" "restore"
|
||||
log_success "Restored the two_factor requirement for $TARGET."
|
||||
else
|
||||
local CONFIRM_EXEMPT=""
|
||||
prompt_yn " $TARGET will be able to log in with just a password (no 2FA) on every domain this instance protects. Continue? (y/n):" "n" CONFIRM_EXEMPT
|
||||
if [[ "$CONFIRM_EXEMPT" =~ ^[Yy]$ ]]; then
|
||||
_authelia_set_2fa_exempt "$CONFIG_FILE" "$TARGET" "exempt"
|
||||
log_success "$TARGET no longer needs 2FA (one_factor only)."
|
||||
else
|
||||
log_info "Left as-is."
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
5)
|
||||
if [ "$IS_ADMIN" = "yes" ]; then
|
||||
_authelia_toggle_admin "$USERS_FILE" "$START" "$END" "false"
|
||||
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
|
||||
log_success "$TARGET demoted from admin."
|
||||
else
|
||||
_authelia_toggle_admin "$USERS_FILE" "$START" "$END" "true"
|
||||
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
|
||||
log_success "$TARGET promoted to admin."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
ACTION="6"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$ACTION" =~ ^[1245]$ ]]; then
|
||||
local RESTART_AUTH=""
|
||||
prompt_yn " Restart Authelia to apply this change? (y/n):" "y" RESTART_AUTH
|
||||
if [ "$RESTART_AUTH" = "y" ] || [ "$RESTART_AUTH" = "Y" ]; then
|
||||
(cd "$AUTHELIA_DIR" && docker compose restart authelia 2>/dev/null) \
|
||||
&& log_success "Authelia restarted" \
|
||||
|| log_warning "Restart failed — check: docker compose logs authelia"
|
||||
fi
|
||||
echo ""
|
||||
prompt_yn " Do something else with $TARGET? (y/n):" "n" CONTINUE
|
||||
else
|
||||
CONTINUE="n"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Enables Authelia's OIDC PROVIDER feature — a distinct thing from the
|
||||
# forward_auth (proxy-auth) setup install_authelia() already does. forward_auth
|
||||
# gates a whole Caddy site behind an Authelia login page before the request
|
||||
|
||||
Reference in New Issue
Block a user