From 13b73671f8938fe734fbf2f7bc5b21353b9f20e9 Mon Sep 17 00:00:00 2001 From: aki Date: Sat, 26 Apr 2025 01:11:16 +0800 Subject: [PATCH] feat(authelia): Add account management functionality to update users and passwords --- update-setup.sh | 558 +++++++++++++++++++++--------------------------- 1 file changed, 244 insertions(+), 314 deletions(-) diff --git a/update-setup.sh b/update-setup.sh index 7ccd653..010585e 100644 --- a/update-setup.sh +++ b/update-setup.sh @@ -4,6 +4,7 @@ # - 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 # Created: April 26, 2025 set -e @@ -231,40 +232,12 @@ update_authelia_config() { if [ -f "$ENV_FILE" ]; then TAILNET_DOMAIN=$(grep -o "TAILSCALE_TAILNET_DOMAIN=.*" "$ENV_FILE" | cut -d'=' -f2 | tr -d '"' | tr -d "'") TAILSCALE_HOSTNAME=$(grep -o "TAILSCALE_HOSTNAME=.*" "$ENV_FILE" | cut -d'=' -f2 | tr -d '"' | tr -d "'") - REDIS_PASSWORD=$(grep -o "AUTHELIA_REDIS_PASSWORD=.*" "$ENV_FILE" | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$TAILNET_DOMAIN" ] && [ -n "$TAILSCALE_HOSTNAME" ]; then - # Use the full Tailnet domain (e.g., "example.ts.net") for cookies - # not just "ts.net" which is a public suffix and not allowed - - # Replace domain placeholder with actual Tailnet domain - sed -i "s/domain: 'your-tailnet.ts.net'/domain: '$TAILNET_DOMAIN'/g" "$AUTHELIA_CONFIG" - - # For access control rules, update both wildcards and direct domain - sed -i "s/domain: '\*.your-tailnet.ts.net'/domain: '\*.$TAILNET_DOMAIN'/g" "$AUTHELIA_CONFIG" - sed -i "s/domain: 'your-tailnet.ts.net'/domain: '$TAILNET_DOMAIN'/g" "$AUTHELIA_CONFIG" - - # For URLs, use the full hostname - sed -i "s/https:\/\/tailscale-nas.your-tailnet.ts.net/https:\/\/$TAILSCALE_HOSTNAME.$TAILNET_DOMAIN/g" "$AUTHELIA_CONFIG" - - # Ensure Redis password is set correctly - if [ -n "$REDIS_PASSWORD" ]; then - # Check if redis connection string exists in the config - if grep -q "redis:" "$AUTHELIA_CONFIG"; then - echo -e "${CYAN}Setting Redis password in Authelia configuration...${NC}" - # Make sure the Redis password in configuration matches the one in .env - sed -i "s/password: \${AUTHELIA_SESSION_REDIS_PASSWORD}/password: $REDIS_PASSWORD/g" "$AUTHELIA_CONFIG" - fi - fi - - echo -e "${GREEN}Configured Authelia with your Tailscale domain:${NC}" - echo -e "${CYAN} - Cookie domain: ${GREEN}$TAILNET_DOMAIN${NC}" - echo -e "${CYAN} - Access control for: ${GREEN}*.$TAILNET_DOMAIN and $TAILNET_DOMAIN${NC}" - echo -e "${CYAN} - Authelia URL: ${GREEN}https://$TAILSCALE_HOSTNAME.$TAILNET_DOMAIN${NC}" - echo -e "${CYAN} - Redis password: ${GREEN}Configured${NC}" - else - echo -e "${YELLOW}Warning: Could not find both TAILSCALE_HOSTNAME and TAILSCALE_TAILNET_DOMAIN in .env${NC}" - echo -e "${YELLOW}You may need to manually edit $AUTHELIA_CONFIG to set your domains correctly${NC}" + # Replace placeholders with actual values + sed -i "s/\*.ts.net/\*.$TAILNET_DOMAIN/g" "$AUTHELIA_CONFIG" + sed -i "s/tailscale-nas.ts.net/$TAILSCALE_HOSTNAME.$TAILNET_DOMAIN/g" "$AUTHELIA_CONFIG" + echo -e "${GREEN}Configured Authelia with your Tailscale domain: $TAILSCALE_HOSTNAME.$TAILNET_DOMAIN${NC}" fi fi @@ -280,234 +253,50 @@ update_arr_config() { local container=$1 local path=$2 - echo -e "${BLUE}Configuring $container URL base and extracting API key...${NC}" + echo -e "${BLUE}Updating ${container} configuration...${NC}" + until [ -f "${CONFIG_ROOT:-.}"/"$container"/config.xml ]; do sleep 1; done + sed -i.bak "s/<\/UrlBase>/\/$path<\/UrlBase>/" "${CONFIG_ROOT:-.}"/"$container"/config.xml && rm "${CONFIG_ROOT:-.}"/"$container"/config.xml.bak - # Check if config file exists - config_file="${CONFIG_ROOT:-.}/$container/config.xml" - timeout=10 - count=0 - while [ ! -f "$config_file" ] && [ $count -lt $timeout ]; do - echo -e "${YELLOW}Waiting for $container config file to be available... ($(($timeout-$count)) seconds remaining)${NC}" - sleep 1 - count=$((count+1)) - done + CONTAINER_NAME_UPPER=$(echo "$container" | tr '[:lower:]' '[:upper:]') + sed -i.bak 's/^'"${CONTAINER_NAME_UPPER}"'_API_KEY=.*/'"${CONTAINER_NAME_UPPER}"'_API_KEY='"$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config.xml)"'/' .env && rm .env.bak - if [ ! -f "$config_file" ]; then - echo -e "${RED}Error: Config file for $container not found after $timeout seconds${NC}" - echo -e "${YELLOW}Is $container configured properly? Check the container logs${NC}" - return 1 - fi - - # Update URL base - if ! grep -q "/$path" "$config_file"; then - echo -e "${BLUE}Setting URL base to /$path${NC}" - sed -i.bak "s|.*|/$path|" "$config_file" 2>/dev/null || sed -i.bak "s||/$path|" "$config_file" 2>/dev/null - if [ $? -ne 0 ]; then - echo -e "${RED}Failed to update URL base in $config_file${NC}" - return 1 - fi - rm -f "${config_file}.bak" - else - echo -e "${GREEN}URL base already set to /$path${NC}" - fi - - # Extract API key - api_key=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "$config_file") - if [ -z "$api_key" ]; then - echo -e "${YELLOW}Could not find API key in $container configuration${NC}" - else - CONTAINER_NAME_UPPER=$(echo "$container" | tr '[:lower:]' '[:upper:]') - current_key=$(grep "^${CONTAINER_NAME_UPPER}_API_KEY=" .env | cut -d'=' -f2) - - if [ "$current_key" != "$api_key" ]; then - echo -e "${BLUE}Updating ${CONTAINER_NAME_UPPER}_API_KEY in .env file${NC}" - sed -i.bak "s|^${CONTAINER_NAME_UPPER}_API_KEY=.*|${CONTAINER_NAME_UPPER}_API_KEY=$api_key|" .env - rm -f .env.bak - else - echo -e "${GREEN}${CONTAINER_NAME_UPPER}_API_KEY already up to date in .env file${NC}" - fi - fi - - echo -e "${GREEN}Restarting $container to apply changes...${NC}" - if ! docker compose restart "$container"; then - echo -e "${RED}Failed to restart $container${NC}" - return 1 - fi - - echo -e "${GREEN}Successfully configured $container${NC}" - return 0 + echo -e "${GREEN}Update of ${container} configuration complete, restarting...${NC}" + docker compose restart "$container" } update_qbittorrent_config() { local container=$1 - echo -e "${BLUE}Configuring $container WebUI password...${NC}" + echo -e "${BLUE}Updating ${container} configuration...${NC}" + docker compose stop "$container" + until [ -f "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf ]; do sleep 1; done + sed -i.bak '/WebUI\\ServerDomains=*/a WebUI\\Password_PBKDF2="@ByteArray(ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==)"' "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf && rm "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf.bak - # Stop the container to ensure we can modify the config - echo -e "${BLUE}Stopping $container to update configuration...${NC}" - if ! docker compose stop "$container"; then - echo -e "${RED}Failed to stop $container${NC}" - return 1 - fi - - # Check if config file exists - config_file="${CONFIG_ROOT:-.}/$container/qBittorrent/qBittorrent.conf" - timeout=10 - count=0 - while [ ! -f "$config_file" ] && [ $count -lt $timeout ]; do - echo -e "${YELLOW}Waiting for $container config file to be available... ($(($timeout-$count)) seconds remaining)${NC}" - sleep 1 - count=$((count+1)) - done - - if [ ! -f "$config_file" ]; then - echo -e "${RED}Error: Config file for $container not found after $timeout seconds${NC}" - echo -e "${YELLOW}Is $container configured properly? Check the container logs${NC}" - # Restart the container anyway - docker compose start "$container" - return 1 - fi - - # Check if password is already set - if grep -q "WebUI\\\\Password_PBKDF2=" "$config_file"; then - echo -e "${GREEN}Password already configured in $container${NC}" - else - echo -e "${BLUE}Setting default password for $container WebUI${NC}" - sed -i.bak '/WebUI\\\\ServerDomains=*/a WebUI\\\\Password_PBKDF2="@ByteArray(ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==)"' "$config_file" - if [ $? -ne 0 ]; then - echo -e "${RED}Failed to update password in $config_file${NC}" - docker compose start "$container" - return 1 - fi - rm -f "${config_file}.bak" - echo -e "${GREEN}Default password set. Username: admin / Password: adminadmin${NC}" - echo -e "${YELLOW}You should change this password in the WebUI settings${NC}" - fi - - echo -e "${GREEN}Starting $container with updated configuration...${NC}" - if ! docker compose start "$container"; then - echo -e "${RED}Failed to start $container${NC}" - return 1 - fi - - echo -e "${GREEN}Successfully configured $container${NC}" - return 0 + echo -e "${GREEN}Update of ${container} configuration complete, restarting...${NC}" + docker compose start "$container" } update_bazarr_config() { local container=$1 - echo -e "${BLUE}Configuring $container URL base and service connections...${NC}" + echo -e "${BLUE}Updating ${container} configuration...${NC}" + until [ -f "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml ]; do sleep 1; done + sed -i.bak "s/base_url: ''/base_url: '\/$container'/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + sed -i.bak "s/use_radarr: false/use_radarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - # Check if config file exists - config_file="${CONFIG_ROOT:-.}/$container/config/config/config.yaml" - timeout=15 - count=0 - while [ ! -f "$config_file" ] && [ $count -lt $timeout ]; do - echo -e "${YELLOW}Waiting for $container config file to be available... ($(($timeout-$count)) seconds remaining)${NC}" - sleep 1 - count=$((count+1)) - done + until [ -f "${CONFIG_ROOT:-.}"/sonarr/config.xml ]; do sleep 1; done + SONARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/sonarr/config.xml) + sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - if [ ! -f "$config_file" ]; then - echo -e "${RED}Error: Config file for $container not found after $timeout seconds${NC}" - echo -e "${YELLOW}Is $container configured properly? Check the container logs${NC}" - return 1 - fi + until [ -f "${CONFIG_ROOT:-.}"/radarr/config.xml ]; do sleep 1; done + RADARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/radarr/config.xml) + sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - # Update URL base - if grep -q "base_url: '/$container'" "$config_file"; then - echo -e "${GREEN}URL base already set to /$container${NC}" - else - echo -e "${BLUE}Setting URL base to /$container${NC}" - sed -i.bak "s|base_url: .*|base_url: '/$container'|" "$config_file" - if [ $? -ne 0 ]; then - echo -e "${RED}Failed to update URL base in $config_file${NC}" - return 1 - fi - rm -f "${config_file}.bak" - fi + sed -i.bak 's/^BAZARR_API_KEY=.*/BAZARR_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak - # Configure Radarr integration - updates_needed=false - if grep -q "use_radarr: false" "$config_file"; then - echo -e "${BLUE}Enabling Radarr integration${NC}" - sed -i.bak "s/use_radarr: false/use_radarr: true/" "$config_file" - rm -f "${config_file}.bak" - updates_needed=true - else - echo -e "${GREEN}Radarr integration already enabled${NC}" - fi - - # Configure Sonarr integration - if grep -q "use_sonarr: false" "$config_file"; then - echo -e "${BLUE}Enabling Sonarr integration${NC}" - sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "$config_file" - rm -f "${config_file}.bak" - updates_needed=true - else - echo -e "${GREEN}Sonarr integration already enabled${NC}" - fi - - # Get Sonarr API key if needed - sonarr_config="${CONFIG_ROOT:-.}/sonarr/config.xml" - if [ -f "$sonarr_config" ]; then - sonarr_api_key=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "$sonarr_config") - if [ -n "$sonarr_api_key" ]; then - echo -e "${BLUE}Setting Sonarr API key and URL in Bazarr config${NC}" - sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $sonarr_api_key/; s|base_url: .*|base_url: /sonarr|; s/ip: .*/ip: sonarr/ }" "$config_file" - rm -f "${config_file}.bak" - updates_needed=true - else - echo -e "${YELLOW}Could not find Sonarr API key${NC}" - fi - else - echo -e "${YELLOW}Sonarr config not found, skipping Sonarr API configuration${NC}" - fi - - # Get Radarr API key if needed - radarr_config="${CONFIG_ROOT:-.}/radarr/config.xml" - if [ -f "$radarr_config" ]; then - radarr_api_key=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "$radarr_config") - if [ -n "$radarr_api_key" ]; then - echo -e "${BLUE}Setting Radarr API key and URL in Bazarr config${NC}" - sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $radarr_api_key/; s|base_url: .*|base_url: /radarr|; s/ip: .*/ip: radarr/ }" "$config_file" - rm -f "${config_file}.bak" - updates_needed=true - else - echo -e "${YELLOW}Could not find Radarr API key${NC}" - fi - else - echo -e "${YELLOW}Radarr config not found, skipping Radarr API configuration${NC}" - fi - - # Extract Bazarr API key for .env - bazarr_api_key=$(sed -n 's/.*apikey: \(.*\)*/\1/p' "$config_file" | head -n 1) - if [ -n "$bazarr_api_key" ]; then - current_key=$(grep "^BAZARR_API_KEY=" .env | cut -d'=' -f2) - if [ "$current_key" != "$bazarr_api_key" ]; then - echo -e "${BLUE}Updating BAZARR_API_KEY in .env file${NC}" - sed -i.bak "s|^BAZARR_API_KEY=.*|BAZARR_API_KEY=$bazarr_api_key|" .env - rm -f .env.bak - else - echo -e "${GREEN}BAZARR_API_KEY already up to date in .env file${NC}" - fi - else - echo -e "${YELLOW}Could not find Bazarr API key${NC}" - fi - - if [ "$updates_needed" = true ]; then - echo -e "${GREEN}Restarting $container to apply changes...${NC}" - if ! docker compose restart "$container"; then - echo -e "${RED}Failed to restart $container${NC}" - return 1 - fi - else - echo -e "${GREEN}No configuration changes needed for $container${NC}" - fi - - echo -e "${GREEN}Successfully configured $container${NC}" - return 0 + echo -e "${GREEN}Update of ${container} configuration complete, restarting...${NC}" + docker compose restart "$container" } update_service_configs() { @@ -529,85 +318,223 @@ update_service_configs() { return 1 fi - # Keep track of what we updated - updated_containers=() - not_running_containers=() - skipped_containers=() - - echo -e "${BLUE}Checking for containers to update...${NC}" - - # Define the list of containers we can update - declare -a configurable_containers=("radarr" "sonarr" "lidarr" "prowlarr" "bazarr" "qbittorrent") - - # First check which containers we should be able to update - for container in "${configurable_containers[@]}"; do - if ! docker ps --format '{{.Names}}' | grep -q "^${container}$"; then - not_running_containers+=("$container") - fi - done - - # Now update the running containers + echo -e "${BLUE}Checking for running containers to update...${NC}" for container in $(docker ps --format '{{.Names}}'); do if [[ "$container" =~ ^(radarr|sonarr|lidarr|prowlarr)$ ]]; then - echo -e "\n${CYAN}Updating ${BOLD}$container${NC} configuration..." - if update_arr_config "$container" "$container"; then - updated_containers+=("$container") - else - skipped_containers+=("$container (error during update)") - fi + update_arr_config "$container" "$container" elif [[ "$container" =~ ^(bazarr)$ ]]; then - echo -e "\n${CYAN}Updating ${BOLD}$container${NC} configuration..." - if update_bazarr_config "$container"; then - updated_containers+=("$container") - else - skipped_containers+=("$container (error during update)") - fi + update_bazarr_config "$container" elif [[ "$container" =~ ^(qbittorrent)$ ]]; then - echo -e "\n${CYAN}Updating ${BOLD}$container${NC} configuration..." - if update_qbittorrent_config "$container"; then - updated_containers+=("$container") - else - skipped_containers+=("$container (error during update)") - fi + update_qbittorrent_config "$container" fi done - # Print summary echo -e "\n${GREEN}${BOLD}Service Configuration Update Complete!${NC}" - echo -e "${BLUE}Summary:${NC}" +} + +################################################## +# Additional utility functions +################################################## + +# Function to generate a random passphrase with random separators and numbers +generate_passphrase() { + local words=( + "apple" "banana" "cherry" "dragon" "eagle" "forest" "guitar" "harbor" + "island" "jungle" "kitchen" "lemon" "mountain" "notebook" "ocean" + "planet" "quiet" "river" "summer" "tiger" "umbrella" "village" + "winter" "xylophone" "yellow" "zebra" "anchor" "beaver" "candle" + "dolphin" "elephant" "falcon" "giraffe" "hamster" "iguana" "jaguar" + ) - if [[ ${#updated_containers[@]} -gt 0 ]]; then - echo -e " ${GREEN}Successfully updated configurations for:${NC}" - for container in "${updated_containers[@]}"; do - echo -e " - ${CYAN}$container${NC}" + local separators=( "-" "_" "." "+" "=" "*" "~" "^" "@" "#" "%" "&" "!" "?" ) + + # Generate a random number between 100 and 999 + local random_num=$((RANDOM % 900 + 100)) + + # Select 3 random words + local selected_words=() + for i in {1..3}; do + local index=$((RANDOM % ${#words[@]})) + selected_words+=(${words[$index]}) + done + + # Select 2 random separators + local separator1=${separators[$((RANDOM % ${#separators[@]}))]} + local separator2=${separators[$((RANDOM % ${#separators[@]}))]} + + # Combine to form passphrase: word1-word2_word3123 + echo "${selected_words[0]}${separator1}${selected_words[1]}${separator2}${selected_words[2]}${random_num}" +} + +################################################## +# PART 6: Authelia Account Management +################################################## + +manage_authelia_accounts() { + print_header "Authelia Account Management" + + # Check if the users_database.yml file exists + 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 + # Create minimal users database file + 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 + + echo -e "${GREEN}Password hash generated successfully.${NC}" + + # Check if user already exists in the file and update + if grep -q "^[[:space:]]*${username}:" "$users_file"; then + # User exists, update entry by commenting out old lines and adding new ones + sed -i "/^[[:space:]]*${username}:/,/^[[:space:]]*[a-zA-Z0-9_-]\+:/ s/^/# UPDATED: /" "$users_file" + # Remove the last comment marker if it matched a different user + sed -i "0,/^# UPDATED: [[:space:]]*[a-zA-Z0-9_-]\+:/ s/^# UPDATED: //" "$users_file" + fi + + # Add user to the file + cat >> "$users_file" <