#!/usr/bin/env bash set -euo pipefail USERS_FILE="${USERS_FILE:-config/users}" DEFAULT_COST="${BCRYPT_COST:-12}" SKIP_RESTART="${SKIP_RESTART:-0}" # --- Utilidades ------------------------------------------------------------- usage() { cat <<'EOF' Gestor de usuarios Radicale Uso: ./radicale_users.sh list ./radicale_users.sh add [--password | --random] ./radicale_users.sh passwd [--password | --random] ./radicale_users.sh delete [--force] ./radicale_users.sh --help Variables: USERS_FILE Ruta del archivo htpasswd (por defecto config/users) SKIP_RESTART Si vale 1, no reinicia el contenedor automáticamente BCRYPT_COST Coste para htpasswd -B (por defecto 12) Sin argumentos se abre el menú interactivo original. EOF } ensure_users_file() { if [[ ! -f "$USERS_FILE" ]]; then echo "⚠️ No existe $USERS_FILE. Creándolo..." mkdir -p "$(dirname "$USERS_FILE")" touch "$USERS_FILE" fi } require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "❌ Necesitas '$1' instalado en el sistema." >&2 exit 1 fi } maybe_restart() { if [[ "$SKIP_RESTART" == "1" ]]; then echo "⚠️ SKIP_RESTART=1, no se reiniciará Radicale automáticamente." return fi echo "🔄 Reiniciando contenedor Radicale..." docker compose restart radicale >/dev/null 2>&1 || true echo "✅ Radicale reiniciado." } generate_password() { if command -v openssl >/dev/null 2>&1; then openssl rand -base64 18 else date +%s | sha256sum | head -c 16 fi } prompt_password() { local pwd confirm read -rs -p "Introduce contraseña: " pwd echo "" read -rs -p "Confirma contraseña: " confirm echo "" if [[ "$pwd" != "$confirm" ]]; then echo "❌ Las contraseñas no coinciden." >&2 exit 1 fi echo "$pwd" } htpasswd_update() { local user="$1" password="$2" htpasswd -B -C "$DEFAULT_COST" -b "$USERS_FILE" "$user" "$password" >/dev/null normalize_bcrypt_prefix "$user" } normalize_bcrypt_prefix() { local user="$1" if ! command -v python3 >/dev/null 2>&1; then echo "⚠️ python3 no disponible: no se pudo normalizar el hash bcrypt de '$user' (prefijo \$2y\$)." >&2 echo " Radicale solo acepta hashes \$2b\$, por lo que este usuario podría fallar." >&2 return fi python3 - "$USERS_FILE" "$user" <<'PY' import pathlib, re, sys path = pathlib.Path(sys.argv[1]) user = sys.argv[2] data = path.read_text() pattern = re.compile(rf'^({re.escape(user)}:\$)2y', re.M) updated, count = pattern.subn(r'\g<1>2b', data) if count: path.write_text(updated) PY } list_users() { ensure_users_file echo "👥 Usuarios actuales:" if [[ -s "$USERS_FILE" ]]; then cut -d':' -f1 "$USERS_FILE" | sort else echo "(ninguno)" fi } cmd_add() { ensure_users_file local user="$1" password="" random=0 shift || true while [[ $# -gt 0 ]]; do case "$1" in --password|-p) password="$2"; shift 2 ;; --random|-r) random=1; shift ;; --no-restart) SKIP_RESTART=1; shift ;; *) echo "❗ Opción desconocida: $1" >&2; exit 1 ;; esac done if [[ -z "$user" ]]; then echo "❌ Debes indicar un usuario." >&2 exit 1 fi if grep -q "^$user:" "$USERS_FILE"; then echo "⚠️ El usuario '$user' ya existe." >&2 exit 1 fi if [[ $random -eq 1 ]]; then password="$(generate_password)" echo "🔐 Contraseña generada para '$user': $password" elif [[ -z "$password" ]]; then password="$(prompt_password)" fi htpasswd_update "$user" "$password" maybe_restart } cmd_passwd() { ensure_users_file local user="$1" password="" random=0 shift || true while [[ $# -gt 0 ]]; do case "$1" in --password|-p) password="$2"; shift 2 ;; --random|-r) random=1; shift ;; --no-restart) SKIP_RESTART=1; shift ;; *) echo "❗ Opción desconocida: $1" >&2; exit 1 ;; esac done if [[ -z "$user" ]]; then echo "❌ Debes indicar usuario." >&2 exit 1 fi if ! grep -q "^$user:" "$USERS_FILE"; then echo "⚠️ El usuario '$user' no existe." >&2 exit 1 fi if [[ $random -eq 1 ]]; then password="$(generate_password)" echo "🔐 Nueva contraseña generada: $password" elif [[ -z "$password" ]]; then password="$(prompt_password)" fi htpasswd_update "$user" "$password" maybe_restart } cmd_delete() { ensure_users_file local user="$1" force=0 shift || true while [[ $# -gt 0 ]]; do case "$1" in --force|-f) force=1; shift ;; --no-restart) SKIP_RESTART=1; shift ;; *) echo "❗ Opción desconocida: $1" >&2; exit 1 ;; esac done if [[ -z "$user" ]]; then echo "❌ Debes indicar usuario." >&2 exit 1 fi if ! grep -q "^$user:" "$USERS_FILE"; then echo "⚠️ El usuario '$user' no existe." >&2 exit 1 fi if [[ $force -eq 0 ]]; then read -rp "¿Eliminar '$user'? (s/N): " confirm [[ "$confirm" =~ ^[sS]$ ]] || { echo "🚫 Operación cancelada."; return; } fi sed -i.bak "/^$user:/d" "$USERS_FILE" rm -f "${USERS_FILE}.bak" echo "❌ Usuario '$user' eliminado." maybe_restart } # --- Menú interactivo legado ----------------------------------------------- interactive_menu() { while true; do echo "========================================" echo "🔐 GESTOR DE USUARIOS RADICALE" echo "========================================" echo "1️⃣ Listar usuarios" echo "2️⃣ Crear nuevo usuario" echo "3️⃣ Editar contraseña de usuario" echo "4️⃣ Eliminar usuario" echo "5️⃣ Salir" echo "----------------------------------------" read -rp "Selecciona una opción [1-5]: " opt echo "" case "$opt" in 1) list_users ;; 2) read -rp "Usuario nuevo: " user; cmd_add "$user" ;; 3) read -rp "Usuario a modificar: " user; cmd_passwd "$user" ;; 4) read -rp "Usuario a eliminar: " user; cmd_delete "$user" ;; 5) echo "👋 Saliendo..."; exit 0 ;; *) echo "❗ Opción no válida." ;; esac echo "" read -rp "Presiona ENTER para continuar..." _ clear done } # --- Punto de entrada ------------------------------------------------------- main() { require_command htpasswd if [[ $# -eq 0 ]]; then interactive_menu exit 0 fi case "$1" in list) list_users ;; add) shift cmd_add "${1:-}" "${@:2}" ;; passwd) shift cmd_passwd "${1:-}" "${@:2}" ;; delete|remove) shift cmd_delete "${1:-}" "${@:2}" ;; --help|-h) usage ;; *) echo "❗ Comando desconocido: $1"; usage; exit 1 ;; esac } main "$@"