#!/bin/bash # Combined update script for docker-compose-nas # - Updates .env file from .env.example while preserving values # - Updates Authelia configuration from example file # - Configures services with correct paths and API keys # - Manages Authelia accounts # - Controls service authentication requirements # Created: April 26, 2025 set -e # Color definitions RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color # Files ENV_FILE=".env" ENV_EXAMPLE=".env.example" TIMESTAMP=$(date +"%Y%m%d-%H%M%S") ENV_BACKUP=".env.${TIMESTAMP}.bak" AUTHELIA_CONFIG="authelia/configuration.yml" AUTHELIA_CONFIG_EXAMPLE="authelia/configuration.example.yml" AUTHELIA_CONFIG_BACKUP="authelia/configuration.${TIMESTAMP}.bak" COMPOSE_FILE="docker-compose.yml" COMPOSE_BACKUP="docker-compose.${TIMESTAMP}.bak" # Check if yq is installed check_yq() { if ! command -v yq &> /dev/null; then echo -e "${YELLOW}Warning: 'yq' is not installed. While not required, it provides better YAML handling.${NC}" echo -e "${YELLOW}Installation instructions: https://github.com/mikefarah/yq#install${NC}" return 1 fi return 0 } # Print section header print_header() { echo -e "\n${CYAN}${BOLD}$1${NC}" echo -e "${CYAN}$(printf '=%.0s' $(seq 1 ${#1}))${NC}" } # Check if a file exists check_file() { if [ ! -f "$1" ]; then echo -e "${RED}Error: $1 doesn't exist${NC}" return 1 fi return 0 } # Function to create a backup create_backup() { echo -e "${BLUE}Creating backup of $1 as $2...${NC}" cp "$1" "$2" } ################################################## # PART 1: Update .env file from .env.example ################################################## update_env_file() { print_header "Environment File Update Tool" # Check if files exist if [ ! -f "$ENV_FILE" ]; then echo -e "${RED}Error: $ENV_FILE doesn't exist${NC}" echo -e "Creating a new $ENV_FILE from $ENV_EXAMPLE" cp "$ENV_EXAMPLE" "$ENV_FILE" echo -e "${GREEN}Done! Please review and fill in required values in $ENV_FILE${NC}" return 0 fi if ! check_file "$ENV_EXAMPLE"; then return 1 fi echo -e "${BLUE}This will update your $ENV_FILE based on the structure in $ENV_EXAMPLE${NC}" echo -e "${BLUE}Your existing values will be preserved where possible${NC}" echo -e "${BLUE}Backup will be created as: $ENV_BACKUP${NC}" echo -e "${YELLOW}Continue? [y/N]:${NC}" read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo -e "${RED}Environment update cancelled.${NC}" return 0 fi # Create backup of current .env create_backup "$ENV_FILE" "$ENV_BACKUP" # Store current env values echo -e "${BLUE}Reading current environment values...${NC}" declare -A current_values declare -A current_keys_present while IFS='=' read -r key value; do # Skip comments and empty lines if [[ ! "$key" =~ ^#.*$ ]] && [[ ! -z "$key" ]]; then # Clean up any comments after the value value=$(echo "$value" | sed 's/[[:space:]]*#.*$//') # Trim leading/trailing whitespace key=$(echo "$key" | xargs) value=$(echo "$value" | xargs) # Store in associative array if key is not empty if [[ ! -z "$key" ]]; then current_values["$key"]="$value" # Track that this key existed in original file, regardless of value current_keys_present["$key"]=1 fi fi done < "$ENV_FILE" # Create new env file from example echo -e "${BLUE}Creating new $ENV_FILE from $ENV_EXAMPLE...${NC}" cp "$ENV_EXAMPLE" "$ENV_FILE.new" # Track which keys from the current env have been used declare -A used_keys # Track new keys that need attention new_keys=() # Track keys with special warnings special_keys=() # Process the template and fill in values from current env while IFS= read -r line; do if [[ "$line" =~ ^([A-Za-z0-9_]+)=(.*)$ ]]; then key="${BASH_REMATCH[1]}" default_value="${BASH_REMATCH[2]}" # Mark the key as used if it exists in the original file if [[ -n "${current_keys_present[$key]}" ]]; then used_keys["$key"]=1 # Replace the line with the current value if one exists if [[ -n "${current_values[$key]}" ]]; then sed -i "s|^$key=.*$|$key=${current_values[$key]}|" "$ENV_FILE.new" fi # If key doesn't exist in original file and has empty/placeholder value elif [[ -z "$default_value" ]] || [[ "$default_value" == '""' ]] || [[ "$default_value" == "''" ]]; then new_keys+=("$key") # Special attention for Authelia keys if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then special_keys+=("$key") fi fi fi done < "$ENV_FILE.new" # Create section for unused/deprecated keys at the bottom of the file echo -e "\n\n# --- DEPRECATED OR UNUSED KEYS (Kept for Reference) ---" >> "$ENV_FILE.new" echo -e "# Keys below were in your original .env but aren't in the current .env.example" >> "$ENV_FILE.new" echo -e "# They may be deprecated or renamed. Review and remove if no longer needed\n" >> "$ENV_FILE.new" unused_keys_count=0 for key in "${!current_values[@]}"; do if [[ -z "${used_keys[$key]}" ]]; then echo "$key=${current_values[$key]} # DEPRECATED/UNUSED - Review" >> "$ENV_FILE.new" unused_keys_count=$((unused_keys_count + 1)) fi done # Replace the old file with the new one mv "$ENV_FILE.new" "$ENV_FILE" # Generate summary echo -e "\n${GREEN}${BOLD}Environment Update Complete!${NC}" echo -e "${BLUE}Summary:${NC}" echo -e " - ${CYAN}Original config backed up to: $ENV_BACKUP${NC}" echo -e " - ${CYAN}Updated .env structure to match .env.example${NC}" echo -e " - ${CYAN}Preserved ${#used_keys[@]} existing values${NC}" if [[ $unused_keys_count -gt 0 ]]; then echo -e " - ${YELLOW}Found $unused_keys_count deprecated/unused keys${NC}" echo -e " ${YELLOW}These have been moved to the bottom of the file with warnings${NC}" fi if [[ ${#new_keys[@]} -gt 0 ]]; then echo -e "\n${YELLOW}${BOLD}NEW KEYS NEEDING ATTENTION:${NC}" echo -e "${YELLOW}The following keys are new and may need values set:${NC}" for key in "${new_keys[@]}"; do echo -e " - ${MAGENTA}$key${NC}" done fi if [[ ${#special_keys[@]} -gt 0 ]]; then echo -e "\n${RED}${BOLD}IMPORTANT SECURITY KEYS:${NC}" echo -e "${RED}The following keys require secure values:${NC}" for key in "${special_keys[@]}"; do echo -e " - ${MAGENTA}$key${NC}" # Specific advice for Authelia keys if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then echo -e " ${CYAN}Generate with: ${GREEN}openssl rand -hex 32${NC}" fi done fi echo -e "\n${BLUE}Review your updated $ENV_FILE file and adjust any values as needed.${NC}" } ################################################## # PART 2: Update Authelia configuration ################################################## update_authelia_config() { print_header "Authelia Configuration Update Tool" # Check if files exist if ! check_file "$AUTHELIA_CONFIG_EXAMPLE"; then echo -e "${RED}Error: Example configuration file '$AUTHELIA_CONFIG_EXAMPLE' doesn't exist${NC}" return 1 fi if ! check_file "$ENV_FILE"; then echo -e "${RED}Error: Environment file '$ENV_FILE' doesn't exist. Cannot retrieve domain settings.${NC}" return 1 fi # Get the tailnet domain and hostname from .env local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") local TAILSCALE_HOSTNAME=$(grep -oP "^TAILSCALE_HOSTNAME=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") local FULL_HOSTNAME="${TAILSCALE_HOSTNAME}.${TAILNET_DOMAIN}" local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}" if [ -z "$TAILNET_DOMAIN" ] || [ -z "$TAILSCALE_HOSTNAME" ]; then echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN or TAILSCALE_HOSTNAME from $ENV_FILE${NC}" return 1 fi # If config file doesn't exist, create it from example if [ ! -f "$AUTHELIA_CONFIG" ]; then echo -e "${YELLOW}Authelia configuration file '$AUTHELIA_CONFIG' doesn't exist, creating from example...${NC}" cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG" echo -e "${GREEN}Created new Authelia configuration file.${NC}" # Proceed to update the newly created file else echo -e "${BLUE}This will update your Authelia configuration '$AUTHELIA_CONFIG' based on '$AUTHELIA_CONFIG_EXAMPLE'${NC}" echo -e "${BLUE}Your Tailscale domain settings from '$ENV_FILE' will be applied.${NC}" echo -e "${BLUE}Backup will be created as: $AUTHELIA_CONFIG_BACKUP${NC}" echo -e "${YELLOW}Continue? [y/N]:${NC}" read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo -e "${RED}Authelia config update cancelled.${NC}" return 0 fi # Create backup of current config only if it exists create_backup "$AUTHELIA_CONFIG" "$AUTHELIA_CONFIG_BACKUP" fi # Check for yq if check_yq; then echo -e "${BLUE}Using 'yq' to update Authelia configuration...${NC}" # Create a temporary file from the example local TEMP_CONFIG="${AUTHELIA_CONFIG}.tmp" cp "$AUTHELIA_CONFIG_EXAMPLE" "$TEMP_CONFIG" # Preserve specific existing values if the original config exists if [ -f "$AUTHELIA_CONFIG_BACKUP" ]; then # Use backup as source of truth for existing values echo -e "${BLUE}Attempting to preserve existing secrets and notifier settings...${NC}" local existing_jwt_secret=$(yq e '.identity_validation.reset_password.jwt_secret // ""' "$AUTHELIA_CONFIG_BACKUP") local existing_session_secret=$(yq e '.session.secret // ""' "$AUTHELIA_CONFIG_BACKUP") local existing_storage_key=$(yq e '.storage.encryption_key // ""' "$AUTHELIA_CONFIG_BACKUP") local existing_redis_pass=$(yq e '.session.redis.password // ""' "$AUTHELIA_CONFIG_BACKUP") local existing_notifier=$(yq e '.notifier // ""' "$AUTHELIA_CONFIG_BACKUP") # Update secrets in temp file if they existed in the backup if [[ -n "$existing_jwt_secret" && "$existing_jwt_secret" != '""' && "$existing_jwt_secret" != "null" ]]; then yq e -i '.identity_validation.reset_password.jwt_secret = strenv(existing_jwt_secret)' --env existing_jwt_secret="$existing_jwt_secret" "$TEMP_CONFIG" ################################################## # PART 5: Authelia Account Management ################################################## # PART 4: Authelia Policy Management ################################################## # Get the current policy for a service from Authelia config # Usage: get_authelia_policy # Output: policy (e.g., one_factor, bypass), not_found, or error get_authelia_policy() { local service=$1 local config_file=$2 if ! check_file "$config_file"; then echo "error: config file not found" return 1 fi # Use yq if available for more reliable YAML parsing if check_yq; then # Find the rule matching the path_regex for the service # Note: This assumes path_regex is unique enough for the service (e.g., ^/service.*) local policy=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\") | .policy" "$config_file" 2>/dev/null) if [ -n "$policy" ] && [ "$policy" != "null" ]; then echo "$policy" else # Check if a rule exists for the service path at all local rule_exists=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\")" "$config_file" 2>/dev/null) if [ -n "$rule_exists" ] && [ "$rule_exists" != "null" ]; then echo "error: policy not found in rule" # Rule exists but policy couldn't be read else echo "not_found" # No rule found for this service path fi fi else # Fall back to grep if yq isn't available (less reliable) # This is fragile and depends heavily on formatting local policy_line=$(grep -A 1 "path_regex: '^/${service}.*'" "$config_file" | grep "policy:") if [ -n "$policy_line" ]; then echo "$policy_line" | awk '{print $2}' else echo "not_found" fi fi return 0 } # Set the policy for a service in Authelia config # Usage: set_authelia_policy set_authelia_policy() { local service=$1 local policy=$2 local config_file=$3 local backup_file="${config_file}.${TIMESTAMP}.bak" if ! check_file "$config_file"; then echo -e "${RED}Error: Authelia configuration file '$config_file' not found.${NC}" return 1 fi if [[ "$policy" != "one_factor" && "$policy" != "two_factor" && "$policy" != "deny" && "$policy" != "bypass" ]]; then echo -e "${RED}Error: Invalid policy '$policy'. Must be one of: one_factor, two_factor, deny, bypass.${NC}" return 1 fi echo -e "${BLUE}Setting policy for service '$service' to '$policy' in '$config_file'...${NC}" # Create backup if it doesn't exist for this run if [ ! -f "$backup_file" ]; then create_backup "$config_file" "$backup_file" fi # Use yq if available if check_yq; then # Check if the rule exists first using the path_regex local rule_index=$(yq e ".access_control.rules | map(.path_regex == \"^/${service}.*\") | indexOf(true)" "$config_file" 2>/dev/null) if [ "$rule_index" == "-1" ] || [ -z "$rule_index" ]; then echo -e "${YELLOW}Warning: No rule found for service path '^/${service}.*' in '$config_file'.${NC}" echo -e "${YELLOW}Attempting to add a new rule...${NC}" # Get the tailnet domain from .env for the new rule local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") if [ -z "$TAILNET_DOMAIN" ]; then echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN from $ENV_FILE. Cannot add rule.${NC}" return 1 fi local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}" # Add the new rule to the access_control.rules array # Places it before the generic domain rule if it exists, otherwise at the end # This assumes a generic rule like "- domain: '*.domain.tld'" exists near the end yq e -i ".access_control.rules |= select(.domain != \"${WILDCARD_DOMAIN}\" or .path_regex != null) + [{\"domain\": \"${WILDCARD_DOMAIN}\", \"path_regex\": \"^/${service}.*\", \"policy\": \"${policy}\"}] + select(.domain == \"${WILDCARD_DOMAIN}\" and .path_regex == null)" "$config_file" if [ $? -eq 0 ]; then echo -e "${GREEN}Added new rule for '$service' with policy '$policy'.${NC}" return 0 else echo -e "${RED}Error: Failed to add new rule for '$service' using yq.${NC}" return 1 fi else # Rule exists, update the policy at the found index yq e -i "(.access_control.rules[$rule_index].policy) = \"$policy\"" "$config_file" if [ $? -eq 0 ]; then echo -e "${GREEN}Policy for '$service' updated to '$policy'.${NC}" return 0 else echo -e "${RED}Error: Failed to update policy for '$service' using yq.${NC}" return 1 fi fi else # Fallback to sed (much less reliable, especially for adding rules) echo -e "${YELLOW}Warning: 'yq' not found. Using 'sed' which is less reliable for YAML manipulation.${NC}" # Check if rule exists (simple grep) if grep -q "path_regex: '^/${service}.*'" "$config_file"; then # Attempt to find the line number of path_regex and update the policy line below it local line_num=$(grep -n "path_regex: '^/${service}.*'" "$config_file" | head -n 1 | cut -d: -f1) # Use head -n 1 just in case if [ -n "$line_num" ]; then # Assuming policy is the next line (fragile!) local policy_line_num=$((line_num + 1)) # Check if the next line actually contains 'policy:' if sed -n "${policy_line_num}p" "$config_file" | grep -q "policy:"; then sed -i "${policy_line_num}s/policy:.*/policy: $policy/" "$config_file" if [ $? -eq 0 ]; then echo -e "${GREEN}Policy for '$service' updated to '$policy' (using sed).${NC}" return 0 else echo -e "${RED}Error: Failed to update policy line using sed.${NC}" return 1 fi else echo -e "${RED}Error: Could not reliably find policy line for '$service' using sed (expected on line $policy_line_num).${NC}" return 1 fi else echo -e "${RED}Error: Could not find line number for service '$service' using sed.${NC}" return 1 fi else echo -e "${RED}Error: Rule for service '$service' not found. Cannot add rule using sed.${NC}" return 1 fi fi } # List services and their Authelia policy status list_authelia_services() { print_header "Authelia Service Policy Status" if ! check_file "$AUTHELIA_CONFIG"; then echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}" return 1 fi echo -e "${BLUE}Checking service policies in $AUTHELIA_CONFIG...${NC}" echo -e "${CYAN}SERVICE\t\tPOLICY${NC}" echo -e "${CYAN}-------\t\t------${NC}" local service_count=0 local processed_services="" # Track processed services # Use yq to get all rules with path_regex if available if check_yq; then # Extract path_regex and policy for rules that have path_regex local rules_data=$(yq e '.access_control.rules[] | select(has("path_regex")) | {"path": .path_regex, "policy": .policy}' "$AUTHELIA_CONFIG") # Process each rule found while IFS= read -r line; do # Extract service name from path_regex (e.g., "^/sonarr.*" -> "sonarr") local path_regex=$(echo "$line" | grep -oP 'path: \K.*' | tr -d '"' | tr -d "'") local policy=$(echo "$line" | grep -oP 'policy: \K.*' | tr -d '"' | tr -d "'") if [[ "$path_regex" =~ ^\^/([a-zA-Z0-9_-]+)\.\* ]]; then local service="${BASH_REMATCH[1]}" # Skip duplicates if multiple rules somehow match the same pattern start if [[ "$processed_services" == *"$service"* ]]; then continue fi processed_services="$processed_services $service" printf "${BOLD}%-20s${NC}" "$service" case "$policy" in "one_factor"|"two_factor") echo -e "${GREEN}${policy}${NC}" ;; "bypass") echo -e "${YELLOW}${policy}${NC}" ;; "deny") echo -e "${RED}${policy}${NC}" ;; *) echo -e "${MAGENTA}Unknown ($policy)${NC}" ;; esac service_count=$((service_count + 1)) fi # Use yq -N to prevent splitting lines with spaces done <<< "$(echo "$rules_data" | yq -N e '.' -)" else # Fallback to grep (less reliable) echo -e "${YELLOW}Warning: yq not found, using grep (may miss services or show duplicates).${NC}" # Find lines with path_regex, then try to get the policy line after grep -n "path_regex: '^/.*" "$AUTHELIA_CONFIG" | while IFS=: read -r line_num line_content; do if [[ "$line_content" =~ path_regex:\ \'^\/([a-zA-Z0-9_-]+)\.\*\' ]]; then local service="${BASH_REMATCH[1]}" # Skip duplicates if [[ "$processed_services" == *"$service"* ]]; then continue fi processed_services="$processed_services $service" # Try to get policy from the next line (very fragile) local policy_line_num=$((line_num + 1)) local policy=$(sed -n "${policy_line_num}p" "$AUTHELIA_CONFIG" | grep "policy:" | awk '{print $2}') printf "${BOLD}%-20s${NC}" "$service" if [ -n "$policy" ]; then case "$policy" in "one_factor"|"two_factor") echo -e "${GREEN}${policy}${NC}" ;; "bypass") echo -e "${YELLOW}${policy}${NC}" ;; "deny") echo -e "${RED}${policy}${NC}" ;; *) echo -e "${MAGENTA}Unknown ($policy)${NC}" ;; esac else echo -e "${RED}Unknown (could not read policy)${NC}" fi service_count=$((service_count + 1)) fi done fi if [ $service_count -eq 0 ]; then echo -e "${YELLOW}No services with path_regex rules found in $AUTHELIA_CONFIG.${NC}" echo -e "${YELLOW}Only services explicitly defined with path_regex rules are listed.${NC}" fi return 0 } cleanup_backups() { print_header "Backup Files Cleanup" echo -e "${BLUE}Searching for backup files...${NC}" local env_backups=$(find . -maxdepth 1 -name ".env.*.bak" | sort) local compose_backups=$(find . -maxdepth 1 -name "docker-compose.*.bak" | sort) local authelia_backups=$(find ./authelia -maxdepth 1 -name "configuration.*.bak" 2>/dev/null | sort) local env_count=$(echo "$env_backups" | grep -c "^") local compose_count=$(echo "$compose_backups" | grep -c "^") local authelia_count=$(echo "$authelia_backups" | grep -c "^") echo -e "${CYAN}Found:${NC}" echo -e " - ${CYAN}$env_count .env backup files${NC}" echo -e " - ${CYAN}$compose_count docker-compose.yml backup files${NC}" echo -e " - ${CYAN}$authelia_count Authelia configuration backup files${NC}" local total_count=$((env_count + compose_count + authelia_count)) if [ $total_count -eq 0 ]; then echo -e "${GREEN}No backup files found.${NC}" return 0 fi echo -e "${YELLOW}Do you want to delete all backup files? [y/N]:${NC}" read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo -e "${YELLOW}Would you like to keep the most recent backup of each type? [Y/n]:${NC}" read -r keep_recent if [[ "$keep_recent" =~ ^[Nn]$ ]]; then echo -e "${RED}Backup cleanup cancelled.${NC}" return 0 fi if [ $env_count -gt 1 ]; then local keep_env=$(echo "$env_backups" | tail -1) local del_env=$(echo "$env_backups" | grep -v "$keep_env") echo -e "${BLUE}Keeping most recent .env backup: ${CYAN}$keep_env${NC}" echo -e "${BLUE}Deleting ${CYAN}$((env_count - 1))${BLUE} older .env backups${NC}" for file in $del_env; do rm "$file" done fi if [ $compose_count -gt 1 ]; then local keep_compose=$(echo "$compose_backups" | tail -1) local del_compose=$(echo "$compose_backups" | grep -v "$keep_compose") echo -e "${BLUE}Keeping most recent docker-compose.yml backup: ${CYAN}$keep_compose${NC}" echo -e "${BLUE}Deleting ${CYAN}$((compose_count - 1))${BLUE} older docker-compose.yml backups${NC}" for file in $del_compose; do rm "$file" done fi if [ $authelia_count -gt 1 ]; then local keep_authelia=$(echo "$authelia_backups" | tail -1) local del_authelia=$(echo "$authelia_backups" | grep -v "$keep_authelia") echo -e "${BLUE}Keeping most recent Authelia config backup: ${CYAN}$keep_authelia${NC}" echo -e "${BLUE}Deleting ${CYAN}$((authelia_count - 1))${BLUE} older Authelia config backups${NC}" for file in $del_authelia; do rm "$file" done fi echo -e "${GREEN}Cleanup completed with most recent backups retained.${NC}" else for file in $env_backups $compose_backups $authelia_backups; do rm "$file" done echo -e "${GREEN}All $total_count backup files have been deleted.${NC}" fi } # Interactive menu for managing Authelia policies manage_authelia_policies() { print_header "Authelia Policy Management" if ! check_file "$AUTHELIA_CONFIG"; then echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}" return 1 fi while true; do echo -e "\n${BLUE}Choose an option:${NC}" echo -e " ${CYAN}1. ${NC}List services and their current Authelia policy" echo -e " ${CYAN}2. ${NC}Set Authelia policy for a service" echo -e " ${CYAN}3. ${NC}Return to main menu" echo local choice echo -e "${YELLOW}Enter your choice [1-3]: ${NC}" read -r choice case "$choice" in 1) list_authelia_services ;; 2) echo -e "${BLUE}Enter the service name to set the policy for (e.g., sonarr, radarr):${NC}" read -r service if [ -z "$service" ]; then echo -e "${RED}No service name provided.${NC}" continue fi echo -e "${BLUE}Select the desired policy for '$service':${NC}" echo -e " ${CYAN}1. ${GREEN}one_factor${NC} (Requires login)" echo -e " ${CYAN}2. ${GREEN}two_factor${NC} (Requires login + 2FA)" echo -e " ${CYAN}3. ${YELLOW}bypass${NC} (No login required)" echo -e " ${CYAN}4. ${RED}deny${NC} (Access denied)" echo -e " ${CYAN}5. ${NC}Cancel" local policy_choice local policy="" while true; do echo -e "${YELLOW}Enter policy choice [1-5]: ${NC}" read -r policy_choice case "$policy_choice" in 1) policy="one_factor"; break ;; 2) policy="two_factor"; break ;; 3) policy="bypass"; break ;; 4) policy="deny"; break ;; 5) policy=""; break ;; # Cancel *) echo -e "${RED}Invalid choice.${NC}" ;; esac done if [ -z "$policy" ]; then echo -e "${YELLOW}Policy change cancelled.${NC}" continue fi # Call the function to set the policy set_authelia_policy "$service" "$policy" "$AUTHELIA_CONFIG" # Check the return status of set_authelia_policy if [ $? -eq 0 ]; then echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}" echo -e " ${CYAN}docker compose restart authelia${NC}" else echo -e "${RED}Failed to set policy. Please check errors above.${NC}" # Optionally offer to restore backup here fi ;; 3) return 0 ;; *) echo -e "${RED}Invalid choice. Please try again.${NC}" ;; esac echo # Add a newline for better readability between menu iterations done } ################################################## # PART 5: Authelia Account Management ################################################## manage_authelia_accounts() { print_header "Authelia Account Management" local users_file="${CONFIG_ROOT:-.}/authelia/users_database.yml" if [ ! -f "$users_file" ]; then echo -e "${RED}Error: users_database.yml not found at $users_file${NC}" echo -e "${YELLOW}Would you like to create a new users database file? [y/N]:${NC}" read -r answer if [[ "$answer" =~ ^[Yy]$ ]]; then cat > "$users_file" </dev/null) if [ -z "$password_hash" ]; then echo -e "${RED}Error: Failed to generate password hash. Is the authelia container available?${NC}" echo -e "${YELLOW}Trying direct docker run method...${NC}" password_hash=$(docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "$password" 2>/dev/null) if [ -z "$password_hash" ]; then echo -e "${RED}Error: Both methods failed to generate a password hash. Skipping user.${NC}" continue fi fi password_hash=$(echo "$password_hash" | sed 's/^Digest: //') if [[ ! "$password_hash" =~ ^\$argon2id.*$ ]]; then echo -e "${RED}Error: Generated hash does not have the expected format. Actual value:${NC}" echo -e "${YELLOW}$password_hash${NC}" echo -e "${RED}Skipping user creation.${NC}" continue fi echo -e "${GREEN}Password hash generated successfully.${NC}" if grep -q "^[[:space:]]*${username}:" "$users_file"; then sed -i "/^[[:space:]]*${username}:/,/^[[:space:]]*[a-zA-Z0-9_-]\+:/ s/^/# UPDATED: /" "$users_file" sed -i "0,/^# UPDATED: [[:space:]]*[a-zA-Z0-9_-]\+:/ s/^# UPDATED: //" "$users_file" fi cat >> "$users_file" < ${NC} Set Authelia policy for a service (e.g., 'one_factor', 'bypass')." echo -e " ${CYAN}cleanup${NC} Interactively clean up old backup files (.bak)." echo -e " ${CYAN}all${NC} Run 'update-env', 'update-authelia', and 'update-services'." echo -e " ${CYAN}help${NC} Show this help message." echo -e "" echo -e "${BLUE}Examples:${NC}" echo -e " $0 update-authelia" echo -e " $0 set-policy sonarr one_factor" echo -e " $0 set-policy radarr bypass" echo -e " $0 manage-policies" echo -e " $0 all" echo -e "" echo -e "${YELLOW}Note:${NC} Some commands require Docker to be running and may restart containers." echo -e "${YELLOW}Policy changes require an Authelia restart ('docker compose restart authelia').${NC}" } # Check if any arguments were provided if [ $# -eq 0 ]; then show_help exit 0 fi # Process command line arguments case "$1" in update-env) update_env_file ;; update-authelia) update_authelia_config ;; update-services) update_service_configs ;; manage-accounts) manage_authelia_accounts # Interactive ;; manage-policies) manage_authelia_policies # Interactive ;; list-policies) list_authelia_services ;; set-policy) if [ -z "$2" ] || [ -z "$3" ]; then echo -e "${RED}Error: Service name and policy are required.${NC}" >&2 echo -e "Usage: $0 set-policy ${NC}" >&2 echo -e "Valid policies: one_factor, two_factor, bypass, deny" >&2 exit 1 fi if ! check_file "$AUTHELIA_CONFIG"; then exit 1; fi # Backup is handled within set_authelia_policy if needed set_authelia_policy "$2" "$3" "$AUTHELIA_CONFIG" if [ $? -eq 0 ]; then echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}" echo -e " ${CYAN}docker compose restart authelia${NC}" fi ;; cleanup) cleanup_backups ;; all) print_header "Running All Updates" update_env_file update_authelia_config update_service_configs echo -e "\n${GREEN}${BOLD}All core updates completed!${NC}" echo -e "${BLUE}Review output for any required actions (e.g., setting new .env variables).${NC}" echo -e "${BLUE}Consider running 'manage-accounts' if needed.${NC}" echo -e "${YELLOW}Remember to restart relevant services or the full stack if necessary.${NC}" ;; help|-h|--help) show_help ;; *) echo -e "${RED}Unknown command: $1${NC}" >&2 echo -e "${BLUE}Run '$0 help' for usage information.${NC}" >&2 exit 1 ;; esac exit 0