# frozen_string_literal: true # adn/tools/cli/tailscale.rb — Subcomando para gestión de identidades Tailscale # ============================================================================== # Este subcomando permite alternar entre diferentes perfiles de Tailscale # (ej. DASUTEN vs Personal) de forma segura utilizando candados. # # Usa `candados.rb sudo` como primitiva reutilizable para ejecutar # comandos con privilegios (no implementa sudo propio). # # Perfiles: # - dasuten: pcdasu0@frlr.utn.edu.ar (ecosistema DASUTEN) # - personal: rmonla@gmail.com (otros ecosistemas/topologías) require_relative '../core/colores' module ADN class SubcomandoTailscale CANDADOS_PATH = File.expand_path('../seguridad/candados.rb', __dir__) def initialize(args, logger) @args = args @logger = logger end def ejecutar if @args.empty? || @args[0] == 'ayuda' mostrar_ayuda return end accion = @args.shift case accion when 'switch' ejecutar_switch when 'status', 'estado' ejecutar_status when 'list', 'listar' ejecutar_list when 'setup' ejecutar_setup else puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}" mostrar_ayuda exit 1 end end private def mostrar_ayuda puts <<~AYUDA #{Color::CYAN}🌐 Gestión de Identidades Tailscale#{Color::RESET} #{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run tailscale [perfil] #{Color::YELLOW}Acciones:#{Color::RESET} switch Alternar a un perfil (dasuten, personal) status Ver estado actual de Tailscale y cuenta activa list Listar perfiles disponibles en Tailscale setup Configurar operador (una vez, evita pedir sudo) #{Color::YELLOW}Perfiles:#{Color::RESET} dasuten Ecosistema DASUTEN (pcdasu0@frlr.utn.edu.ar) personal Otros ecosistemas (rmonla@gmail.com) #{Color::YELLOW}Ejemplos:#{Color::RESET} ./adn/tools/run tailscale setup ./adn/tools/run tailscale switch dasuten ./adn/tools/run tailscale switch personal ./adn/tools/run tailscale status AYUDA end # ─── Status ─────────────────────────────────────────────────────── def ejecutar_status puts "#{Color::CYAN}🔍 Estado actual de Tailscale:#{Color::RESET}" puts "#{Color::DIM}#{'─' * 60}#{Color::RESET}" system('tailscale status') puts "" cuenta = `tailscale status --json 2>/dev/null | jq -r '.User[].LoginName' 2>/dev/null`.strip if cuenta.empty? || cuenta == 'null' puts "#{Color::YELLOW}⚠ No se pudo determinar la cuenta activa.#{Color::RESET}" else puts "#{Color::BOLD}Cuenta activa:#{Color::RESET} #{Color::GREEN}#{cuenta}#{Color::RESET}" end operador_ok = system('tailscale switch --list > /dev/null 2>&1') if operador_ok puts "#{Color::GREEN}✓ Operador configurado (switch sin sudo)#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Operador NO configurado. Ejecutá: ./adn/tools/run tailscale setup#{Color::RESET}" end end # ─── List ───────────────────────────────────────────────────────── def ejecutar_list puts "#{Color::CYAN}📋 Perfiles disponibles en Tailscale:#{Color::RESET}" # Intentar sin sudo (operador configurado) output = `tailscale switch --list 2>&1` if $?.success? puts output else # Fallback: usar candados sudo (reutilizable) candados_sudo('tailscale', 'switch', '--list') end end # ─── Setup ──────────────────────────────────────────────────────── def ejecutar_setup @logger.info("Configurando operador Tailscale para #{ENV['USER']}...") puts "#{Color::CYAN}⚙️ Configurando operador Tailscale...#{Color::RESET}" puts "#{Color::DIM}Esto permite gestionar Tailscale sin sudo en el futuro.#{Color::RESET}" success = candados_sudo('tailscale', 'set', "--operator=#{ENV['USER']}") if success puts "#{Color::GREEN}✅ Operador configurado exitosamente.#{Color::RESET}" @logger.exito("Operador Tailscale configurado para #{ENV['USER']}") else puts "#{Color::RED}✗ No se pudo configurar el operador.#{Color::RESET}" puts " Verificá la clave sudo en la bóveda: ruby candados.rb ayuda" end end # ─── Switch ─────────────────────────────────────────────────────── def ejecutar_switch perfil = @args.shift if perfil.nil? puts "#{Color::RED}✗ Falta nombre del perfil (dasuten|personal)#{Color::RESET}" return end case perfil.downcase when 'dasuten', 'd' switch_to('dasuten') when 'personal', 'p' switch_to('personal') else puts "#{Color::YELLOW}⚠ Perfil no reconocido. Perfiles válidos: dasuten, personal#{Color::RESET}" end end def switch_to(tipo) @logger.info("Cambiando perfil Tailscale a: #{tipo}") # 1. Obtener usuario de la bóveda clave_user = tipo == 'dasuten' ? 'tailscale_dasuten_user' : 'tailscale_personal_user' user = obtener_secreto(clave_user) if user.nil? || user.empty? @logger.error("No se encontró '#{clave_user}' en la bóveda.") puts "#{Color::RED}✗ Credencial '#{clave_user}' no encontrada en candados.#{Color::RESET}" return end puts "#{Color::CYAN}🔄 Cambiando a perfil: #{Color::BOLD}#{user}#{Color::RESET}..." # 2. Intentar sin sudo (operador configurado) if system("tailscale switch '#{user}' > /dev/null 2>&1") exito_switch(tipo, user) return end # 3. Fallback: usar candados sudo (primitiva reutilizable) puts "#{Color::DIM}Usando candados sudo...#{Color::RESET}" if candados_sudo('tailscale', 'switch', user) exito_switch(tipo, user) return end # 4. Perfil sin sesión → login manual @logger.advertencia("El perfil #{tipo} no tiene sesión activa.") puts "#{Color::YELLOW}⚠ El perfil '#{user}' no tiene sesión en este equipo.#{Color::RESET}" puts " Necesitás iniciar sesión manualmente:" puts " #{Color::BOLD}ruby candados.rb sudo tailscale login#{Color::RESET}" end def exito_switch(tipo, user) puts "#{Color::GREEN}✅ Perfil Tailscale cambiado a '#{tipo}' (#{user}).#{Color::RESET}" @logger.exito("Perfil Tailscale cambiado a #{tipo} (#{user})") puts "" ejecutar_status end # ─── Utilidades ─────────────────────────────────────────────────── # Obtiene un secreto de la bóveda (requiere sesión abierta) def obtener_secreto(clave) output = `ruby #{CANDADOS_PATH} get #{clave} 2>/dev/null`.strip return nil if output.empty? || output.include?('Error') || output.include?('✗') output end # Ejecuta un comando con sudo usando candados.rb sudo (primitiva reutilizable) def candados_sudo(*cmd_parts) system("ruby #{CANDADOS_PATH} sudo #{cmd_parts.join(' ')}") end end end