docs: Actualización de contexto IA, Drones, Candados y nuevos nodos
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"rsa": "eyJpdiI6IkpoTmpZRWZMeE12eWhXSVMiLCJ0YWciOiJUUnJGY05qRlVoOTl2REplTms0ZU5RPT0iLCJkYXRhIjoiNkFIWCsyYm9ndzZKbGM3c0hRPT0ifQ==",
|
||||
"domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||
"admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||
"win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==",
|
||||
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=",
|
||||
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
|
||||
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==",
|
||||
"ns8:rmonla:sudo": "eyJpdiI6IkRqbTBhVlZSYUpVdno3TFkiLCJ0YWciOiJJSnE2ei8zZHIrV2hFQks1VjNpVGN3PT0iLCJkYXRhIjoiT3JxTjRhTmlQK2R3TlJZZk9BPT0ifQ==",
|
||||
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0=",
|
||||
"srv-ns8:rmonla:sudo": "eyJpdiI6InJDOUp2STdYR1crb2NkMTgiLCJ0YWciOiJHSUZVbXM5VnVmZ3d1NXQ3SFVwOUdRPT0iLCJkYXRhIjoib1ZVTzdaLzk1WGdwZDhKREM3aWpnV3c9In0=",
|
||||
"dasu-sql2:Administrator": "eyJpdiI6IlJ0VWJzblVGM2NhWEtvNlgiLCJ0YWciOiJtM0llb2luUmRUZng0NnBsL1JtKzl3PT0iLCJkYXRhIjoiSHVhaklOTFlsSm1PbDJkTjNoYlF0UT09In0=",
|
||||
"srvv-nginx-rm:root": "eyJpdiI6Ijc0STVkTk92Tk5LZEZZWWciLCJ0YWciOiJlOTAwcGpTa21PZFVhN2FJTzIxU1VBPT0iLCJkYXRhIjoiYUJRR3hRakFub1F1OCtyTVdWcW8ifQ==",
|
||||
"srv-ns8:root": "eyJpdiI6Ik9UbHBieDJhU0JZbFFEM0wiLCJ0YWciOiJZeWJ5K0lUUFdOcEQwTmJ5UzhwcVFRPT0iLCJkYXRhIjoiNUE1QW5yenZ0QldjNEdVNXVBPT0ifQ==",
|
||||
"srv-ns8:rsa:passphrase": "eyJpdiI6ImpwRnFPYTZJdEtnUXdxT00iLCJ0YWciOiJMcXRBQ0lOT0paUE50Qjgva3lHSElRPT0iLCJkYXRhIjoiRDUzOFNuY3V1RUZXVHV4U3NnPT0ifQ==",
|
||||
"srv-ns8:rmonla:sudo": "eyJpdiI6Ilg2VzhIMWN6Y2xYVzR3ZzgiLCJ0YWciOiJuWDk0SDcraVhJQTQxd3I3bEpLRFBnPT0iLCJkYXRhIjoiSllyRy9ScXFBRFBFc0l5emRRPT0ifQ==",
|
||||
"srv-ns8:rmonla:rsa": "eyJpdiI6IkpoTmpZRWZMeE12eWhXSVMiLCJ0YWciOiJUUnJGY05qRlVoOTl2REplTms0ZU5RPT0iLCJkYXRhIjoiNkFIWCsyYm9ndzZKbGM3c0hRPT0ifQ==",
|
||||
"srvv-dns:rmonla:sudo": "eyJpdiI6InkyY01pSHlNeGFzZ2lDcnkiLCJ0YWciOiJqYTdkaFM0bTBNMWZhTWxJRUtGTWtBPT0iLCJkYXRhIjoiWmVYbzRWRjNNS244UktuaFVnPT0ifQ==",
|
||||
"srvv-DNS:rmonla:sudo": "eyJpdiI6InkyY01pSHlNeGFzZ2lDcnkiLCJ0YWciOiJqYTdkaFM0bTBNMWZhTWxJRUtGTWtBPT0iLCJkYXRhIjoiWmVYbzRWRjNNS244UktuaFVnPT0ifQ=="
|
||||
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
|
||||
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0=",
|
||||
"dasu-dc:admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||
"srvv-nginx-rm:root": "eyJpdiI6Ijc0STVkTk92Tk5LZEZZWWciLCJ0YWciOiJlOTAwcGpTa21PZFVhN2FJTzIxU1VBPT0iLCJkYXRhIjoiYUJRR3hRakFub1F1OCtyTVdWcW8ifQ==",
|
||||
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==",
|
||||
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=",
|
||||
"srv-ns8:rmonla": "eyJpdiI6IlJ4cHZkbFpJQWVsTTl3T1YiLCJ0YWciOiJ3WTZGMDEvNHNsZm1wZm5jMEF4Q1lBPT0iLCJkYXRhIjoibFljUHRYTm44bUxvQjhCa2tBPT0ifQ==",
|
||||
"dasu-sql3:Administrador": "eyJpdiI6IlRiMDh3U1N2aVhTcDYzbzciLCJ0YWciOiI5NVNyUDFlMXlrM0dGRlNqUXhLbEt3PT0iLCJkYXRhIjoiVkp2V09zSjczcS96UktTSlZTOFdBdz09In0="
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1775576757
|
||||
1775682230
|
||||
@@ -1,31 +1,43 @@
|
||||
# 🔐 candados: Gestión Segura de Secretos
|
||||
# 🔐 candados: Acceso Seguro a Contraseñas (Sin Exponer)
|
||||
|
||||
## Resumen Ejecutivo
|
||||
`candados` es la herramienta central de seguridad del ecosistema ADN. Cifra y gestiona secretos con **AES-256-GCM**, implementando un sistema de **Candado Temporal** que requiere autorización explícita (5 min) antes de liberar credenciales.
|
||||
`candados` es la herramienta central de seguridad del ecosistema ADN. Cifra y gestiona secretos con **AES-256-GCM**, implementando un sistema de **Candado Temporal** que requiere autorización explícita (30 min) antes de liberar credenciales.
|
||||
|
||||
**Principio fundamental:** Las contraseñas **NUNCA** deben aparecer en pantalla, logs ni archivos temporales.
|
||||
**Principio fundamental:** Las contraseñas **NUNCA** deben aparecer en pantalla, logs ni archivos temporales visibles.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Arquitectura
|
||||
- **Cripto**: Motor AES-256-GCM con IV aleatorio y autenticación.
|
||||
- **Bóveda**: Persistencia cifrada en `.boveda.json` (permisos 600).
|
||||
- **MFA Temporal**: Sesiones de 5 min via `.session` (permisos 600).
|
||||
- **Master Key**: `.master.key` local (permisos 600, nunca se comparte).
|
||||
## ⚠️ REGLA DE ORO — NUNCA MOSTRAR CONTRASEÑAS
|
||||
|
||||
## 🚀 Comando Primario: `run` ★
|
||||
Esta herramienta está diseñada para que las contraseñas **NUNCA** se muestren en pantalla ni queden registradas en logs, históricos de shell o archivos temporales visibles.
|
||||
|
||||
El comando `run` es la forma **recomendada** de usar credenciales. Inyecta `USR` y `PASS` directamente en el entorno del subproceso sin imprimir nada:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 🛡️ NUNCA uses `get` en comandos automatizados o logs │
|
||||
│ │
|
||||
│ ✅ CORRECTO: ruby candados.rb run <clave> 'ssh $USR@host' │
|
||||
│ ❌ INCORRECTO: ruby candados.rb get <clave> | algo │
|
||||
│ │
|
||||
│ El comando `get` imprime la contraseña a stdout → queda en logs │
|
||||
│ El comando `run` inyecta USR/PASS en el entorno → no deja rastro │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 MÉTODOS PARA CARGAR CONTRASEÑAS EN VARIABLES
|
||||
|
||||
### ★ MÉTODO 1: `run` (PRIMARIO - Recomendado para comandos)
|
||||
|
||||
Ejecuta un comando con `USR` y `PASS` inyectadas en su entorno. La contraseña **NUNCA** se imprime ni queda en logs.
|
||||
|
||||
```bash
|
||||
# 1. Autorizar (dura 5 min)
|
||||
ruby candados.rb authorize
|
||||
# Sintaxis
|
||||
ruby candados.rb run <clave> 'comando que usa $USR y $PASS'
|
||||
|
||||
# 2. Ejecutar comando con credenciales inyectadas
|
||||
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host "comando"'
|
||||
|
||||
# Retrocompatible con variable legacy (SSHPASS, etc.)
|
||||
ruby candados.rb run admindasu SSHPASS 'sshpass -e ssh root@host'
|
||||
# Ejemplos
|
||||
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host'
|
||||
ruby candados.rb run srv-ns8:root 'ssh $USR@$HOSTNAME "comando"'
|
||||
```
|
||||
|
||||
**¿Qué pasa internamente?**
|
||||
@@ -35,6 +47,30 @@ ruby candados.rb run admindasu SSHPASS 'sshpass -e ssh root@host'
|
||||
4. Limpia las variables al terminar
|
||||
5. **Nada se imprime en pantalla ni queda en logs**
|
||||
|
||||
### ★ MÉTODO 2: `load` (Para shell interactivo)
|
||||
|
||||
Carga `USR` y `PASS` en variables de entorno del shell actual. Usa un archivo temporal (600) que se auto-elimina tras el source.
|
||||
|
||||
```bash
|
||||
# Sintaxis
|
||||
eval $(ruby candados.rb load <clave>)
|
||||
|
||||
# Ejemplo
|
||||
eval $(ruby candados.rb load srv-dasu:rmonla)
|
||||
# → Carga $USR y $PASS en tu shell actual
|
||||
sshpass -p $PASS ssh $USR@servidor
|
||||
```
|
||||
|
||||
**Seguridad:** El archivo temporal tiene permisos `600` y se auto-borra inmediatamente después del `source`.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Arquitectura
|
||||
- **Cripto**: Motor AES-256-GCM con IV aleatorio y autenticación.
|
||||
- **Bóveda**: Persistencia cifrada en `.boveda.json` (permisos 600).
|
||||
- **MFA Temporal**: Sesiones de 30 min via `.session` (permisos 600).
|
||||
- **Master Key**: `.master.key` local (permisos 600, nunca se comparte).
|
||||
|
||||
## 🔒 Comando: `sudo`
|
||||
|
||||
Ejecuta comandos con privilegios, resolviendo la contraseña automáticamente:
|
||||
@@ -49,20 +85,7 @@ La clave se busca automáticamente:
|
||||
2. Variantes normalizadas del hostname
|
||||
3. Fallback genérico: `sudo`
|
||||
|
||||
## 📦 Comando Secundario: `load`
|
||||
|
||||
Para uso interactivo cuando se necesitan las variables en el shell actual. **Nunca imprime la contraseña:**
|
||||
|
||||
```bash
|
||||
eval $(ruby candados.rb load srv-dasu:rmonla)
|
||||
# USR y PASS están disponibles en el shell
|
||||
```
|
||||
|
||||
Internamente escribe los exports a un archivo temporal (`tmp/.candados_env_<hex>`) con permisos `600`, y a stdout solo emite el comando `source` + autoborrado.
|
||||
|
||||
**Nota:** Los temporales se guardan en `tmp/` del repositorio (no en `/tmp/`) para cumplir con el principio 11 de `05_ia.md` (Workspace Bounds).
|
||||
|
||||
## 🔑 Resolución Automática de Pares
|
||||
## 🔑 Resolución Automática de Claves
|
||||
|
||||
Los comandos `run` y `load` detectan automáticamente el par usuario/contraseña:
|
||||
|
||||
@@ -77,12 +100,13 @@ Los comandos `run` y `load` detectan automáticamente el par usuario/contraseña
|
||||
| Comando | Seguridad | Descripción |
|
||||
|---|---|---|
|
||||
| `run <clave> <cmd>` | ✅ Nada expuesto | **★ PRIMARIO** - Ejecuta con USR/PASS inyectados |
|
||||
| `sudo <cmd>` | ✅ Nada expuesto | Ejecuta con privilegios desde bóveda |
|
||||
| `load <clave>` | ✅ Archivo temporal 600 | Carga en shell (usar con `eval`) |
|
||||
| `get <clave>` | ⚠ Imprime a stdout | Obtener valor directo (usar con cuidado) |
|
||||
| `authorize` | ✅ | Abrir candado (5 min) |
|
||||
| `sudo <cmd>` | ✅ Nada expuesto | Ejecuta con privilegios desde bóveda |
|
||||
| `authorize` | ✅ | Abrir candado (30 min) |
|
||||
| `cerrar` | ✅ | Cerrar sesión manualmente |
|
||||
| `set <clave>` | ✅ Input sin eco | Guardar secreto |
|
||||
| `list` | ✅ | Listar claves |
|
||||
| `list` | ✅ | Listar claves (no muestra valores) |
|
||||
| `get <clave>` | ⚠ Imprime a stdout | Obtener valor directo (⚠ solo uso manual) |
|
||||
|
||||
## 🛡️ Auditoría
|
||||
|
||||
@@ -93,12 +117,14 @@ Archivo `access.log` (permisos 600) registra:
|
||||
|
||||
## 🧠 Contexto para IA
|
||||
|
||||
1. **Siempre** ejecutar `authorize` antes de operar
|
||||
2. **Usar `run`** como método primario (inyecta, no imprime)
|
||||
3. **Evitar `get`** en comandos automatizados (filtra en logs)
|
||||
4. Cerrar sesión con `cerrar` cuando ya no se necesite
|
||||
1. **Siempre** ejecutar `authorize` antes de operar (sesión de 30 min)
|
||||
2. **Usar `run`** como método primario (inyecta USR/PASS, no imprime)
|
||||
3. **Usar `load`** para shell interactivo (eval + source)
|
||||
4. **Evitar `get`** en automatizaciones (imprime la contraseña)
|
||||
5. **Cerrar sesión** con `cerrar` cuando ya no se necesite
|
||||
|
||||
```bash
|
||||
# Flujo típico
|
||||
ruby candados.rb authorize
|
||||
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host "ls"'
|
||||
ruby candados.rb cerrar
|
||||
|
||||
+185
-41
@@ -258,7 +258,14 @@ def cmd_run(cripto, boveda, clave, *args)
|
||||
token_user = boveda.get("#{prefijo}_user")
|
||||
user_value = cripto.decrypt(token_user) if token_user
|
||||
elsif clave.include?(':')
|
||||
user_value = clave.split(':').last
|
||||
# Formato: <nodo>:<usuario>[:<tipo>]
|
||||
# Ej: "srv-ns8:rmonla" → usuario="rmonla"
|
||||
# "srv-ns8:rmonla:sudo" → usuario="rmonla" (no "sudo")
|
||||
partes = clave.split(':')
|
||||
if partes.size >= 2
|
||||
# El usuario es siempre la segunda parte (índice 1)
|
||||
user_value = partes[1]
|
||||
end
|
||||
else
|
||||
token_user = boveda.get("#{clave}_user")
|
||||
user_value = cripto.decrypt(token_user) if token_user
|
||||
@@ -366,6 +373,64 @@ def cmd_load(cripto, boveda, clave)
|
||||
$stderr.puts "#{C::GREEN}✓ Credenciales cargadas en variables USR y PASS (archivo temporal seguro).#{C::RESET}"
|
||||
end
|
||||
|
||||
# ─── Comando ssh (ejecutar comando remoto con password de bóveda) ─────
|
||||
# Uso: candados ssh <clave> <usuario@host> '<comando>'
|
||||
# - clave: <nodo>:<usuario> para SSH (busca automáticamente <nodo>:<usuario>:sudo para sudo)
|
||||
# - Si existe clave :sudo separada, la usa para comandos con sudo
|
||||
def cmd_ssh(cripto, boveda, clave, usuario_host, *cmd_args)
|
||||
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
|
||||
abort "#{C::RED}✗ Falta usuario@host.#{C::RESET}" if usuario_host.nil? || usuario_host.empty?
|
||||
abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if cmd_args.nil? || cmd_args.empty?
|
||||
|
||||
verificar_sesion!
|
||||
|
||||
# Obtener password SSH
|
||||
token_ssh = boveda.get(clave)
|
||||
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token_ssh.nil?
|
||||
ssh_pass = cripto.decrypt(token_ssh)
|
||||
safe_ssh_pass = ssh_pass.gsub("'", "'\\''")
|
||||
|
||||
# Buscar automáticamente clave :sudo si existe
|
||||
comando = cmd_args.join(' ')
|
||||
sudo_pass = ssh_pass
|
||||
|
||||
# Intentar buscar clave :sudo basada en la clave SSH
|
||||
if clave.include?(':') && !clave.end_with?(':sudo')
|
||||
# Construir clave sudo: <nodo>:<usuario>:sudo
|
||||
partes = clave.split(':')
|
||||
if partes.size >= 2
|
||||
clave_sudo = "#{partes[0]}:#{partes[1]}:sudo"
|
||||
token_sudo = boveda.get(clave_sudo)
|
||||
if token_sudo
|
||||
sudo_pass = cripto.decrypt(token_sudo)
|
||||
$stderr.puts "#{C::DIM}ℹ Usando clave sudo: #{clave_sudo}#{C::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
safe_sudo_pass = sudo_pass.gsub("'", "'\\''")
|
||||
|
||||
registrar_acceso('SSH', "Clave: #{clave}, Host: #{usuario_host}, Cmd: #{comando}")
|
||||
|
||||
# Escapar comando para bash remoto: reemplazar ' con '\''
|
||||
comando_escaped = comando.gsub("'", "'\\''")
|
||||
|
||||
# Crear script temporal con el comando y password para sudo
|
||||
require 'securerandom'
|
||||
require 'fileutils'
|
||||
FileUtils.mkdir_p(TMP_DIR) unless File.exist?(TMP_DIR)
|
||||
script_path = File.join(TMP_DIR, ".ssh_sudo_#{SecureRandom.hex(4)}")
|
||||
File.write(script_path, "#!/bin/bash\nexport DEBIAN_FRONTEND=noninteractive\necho '#{safe_sudo_pass}' | sudo -S -p '' bash -c '#{comando_escaped}'\n")
|
||||
File.chmod(0700, script_path)
|
||||
|
||||
# Ejecutar: sshpass para SSH, luego script remoto que hace sudo
|
||||
result = system("sshpass -p '#{safe_ssh_pass}' ssh -o StrictHostKeyChecking=no -tt #{usuario_host} 'bash -s' < '#{script_path}'")
|
||||
FileUtils.rm_f(script_path)
|
||||
exit result ? 0 : 1
|
||||
end
|
||||
|
||||
# ─── Ayuda mejorada para cmd_help_resumida ─────────────────────────────
|
||||
|
||||
# ─── Comando sudo (primitiva reutilizable) ─────────────────────
|
||||
# Resolución inteligente de la clave sudo:
|
||||
# 1. <hostname>:<usuario>:sudo (convención específica)
|
||||
@@ -426,58 +491,130 @@ def cmd_sudo(cripto, boveda, comando_args)
|
||||
exit $?.exitstatus || 0
|
||||
end
|
||||
|
||||
def cmd_help
|
||||
def cmd_help_resumida
|
||||
puts <<~HELP
|
||||
#{C::CYAN}🔐 candados — Gestión segura de secretos#{C::RESET}
|
||||
#{C::CYAN}╔═══════════════════════════════════════════════════════════════════╗#{C::RESET}
|
||||
#{C::CYAN}║#{C::RESET} 🔐 #{C::BOLD}candados — Acceso Seguro a Contraseñas (Sin Exponer)#{C::RESET} #{C::CYAN}║#{C::RESET}
|
||||
#{C::CYAN}╚═══════════════════════════════════════════════════════════════════╝#{C::RESET}
|
||||
|
||||
#{C::GREEN}Uso:#{C::RESET} ruby adn/tools/seguridad/candados.rb <comando> [args]
|
||||
#{C::RED}⚠️ REGLA DE ORO: NUNCA MOSTRAR CONTRASEÑAS#{C::RESET}
|
||||
|
||||
#{C::YELLOW}━━━ Comandos principales ━━━#{C::RESET}
|
||||
#{C::BOLD}run <clave> <cmd>#{C::RESET} #{C::GREEN}★ PRIMARIO#{C::RESET} Ejecutar comando con USR y PASS inyectados
|
||||
#{C::BOLD}sudo <comando>#{C::RESET} Ejecutar con privilegios (resuelve clave automáticamente)
|
||||
#{C::BOLD}load <clave>#{C::RESET} Cargar USR/PASS en shell actual (usar con eval)
|
||||
#{C::GREEN}┌─────────────────────────────────────────────────────────────────┐#{C::RESET}
|
||||
#{C::GREEN}│#{C::RESET} #{C::BOLD}MÉTODOS PARA CARGAR CONTRASEÑAS EN VARIABLES#{C::RESET} #{C::GREEN}│#{C::RESET}
|
||||
#{C::GREEN}└─────────────────────────────────────────────────────────────────┘#{C::RESET}
|
||||
|
||||
#{C::YELLOW}━━━ Gestión de bóveda ━━━#{C::RESET}
|
||||
authorize Abrir candado (autorización temporal 30 min)
|
||||
cerrar Cerrar candado manualmente
|
||||
set <clave> Guardar secreto (input seguro, sin eco)
|
||||
get <clave> Obtener secreto (⚠ imprime a stdout)
|
||||
rm <clave> Eliminar secreto
|
||||
list Listar claves disponibles
|
||||
#{C::BOLD}★ run <clave> 'comando'#{C::RESET} → Ejecuta con $USR y $PASS inyectadas (PRIMARIO)
|
||||
#{C::BOLD}★ load <clave>#{C::RESET} → Carga $USR y $PASS en shell (usar con eval)
|
||||
|
||||
#{C::DIM}Ejemplo: ruby candados.rb run srv-ns8:rmonla 'sshpass -p $PASS ssh $USR@host'#{C::RESET}
|
||||
|
||||
#{C::YELLOW}━━━ Comandos ━━━#{C::RESET}
|
||||
#{C::BOLD}run#{C::RESET} <clave> <cmd> #{C::DIM}Ejecutar con credenciales inyectadas#{C::RESET}
|
||||
#{C::BOLD}load#{C::RESET} <clave> #{C::DIM}Cargar en shell actual (eval)#{C::RESET}
|
||||
#{C::BOLD}sudo#{C::RESET} <comando> #{C::DIM}Ejecutar con privilegios#{C::RESET}
|
||||
#{C::BOLD}ssh#{C::RESET} <clave> <u@h> <cmd> #{C::DIM}SSH remoto con password de bóveda#{C::RESET}
|
||||
#{C::BOLD}authorize#{C::RESET} #{C::DIM}Abrir candado (30 min)#{C::RESET}
|
||||
#{C::BOLD}list#{C::RESET} #{C::DIM}Listar claves disponibles#{C::RESET}
|
||||
#{C::BOLD}set/rm#{C::RESET} <clave> #{C::DIM}Guardar/Eliminar secreto#{C::RESET}
|
||||
|
||||
#{C::DIM}Usá#{C::RESET} ruby candados.rb --help #{C::DIM}para ayuda completa y ejemplos detallados.#{C::RESET}
|
||||
HELP
|
||||
end
|
||||
|
||||
def cmd_help_completo
|
||||
puts <<~HELP
|
||||
#{C::CYAN}╔═══════════════════════════════════════════════════════════════════╗#{C::RESET}
|
||||
#{C::CYAN}║#{C::RESET} 🔐 #{C::BOLD}candados — Acceso Seguro a Contraseñas (Sin Exponer)#{C::RESET} #{C::CYAN}║#{C::RESET}
|
||||
#{C::CYAN}╚═══════════════════════════════════════════════════════════════════╝#{C::RESET}
|
||||
|
||||
#{C::RED}⚠️ REGLA DE ORO: NUNCA MOSTRAR CONTRASEÑAS#{C::RESET}
|
||||
Esta herramienta está diseñada para que las contraseñas #{C::BOLD}NUNCA#{C::RESET} se muestren en pantalla
|
||||
ni queden registradas en logs, históricos de shell o archivos temporales visibles.
|
||||
|
||||
#{C::GREEN}┌─────────────────────────────────────────────────────────────────┐#{C::RESET}
|
||||
#{C::GREEN}│#{C::RESET} #{C::BOLD}MÉTODOS PARA CARGAR CONTRASEÑAS EN VARIABLES#{C::RESET} #{C::GREEN}│#{C::RESET}
|
||||
#{C::GREEN}└─────────────────────────────────────────────────────────────────┘#{C::RESET}
|
||||
|
||||
#{C::BOLD}★ MÉTODO 1: run (PRIMARIO - Recomendado para comandos)#{C::RESET}
|
||||
Ejecuta un comando con USR y PASS inyectadas en su entorno.
|
||||
La contraseña #{C::BOLD}NUNCA#{C::RESET} se imprime ni queda en logs.
|
||||
|
||||
#{C::DIM}ruby candados.rb run <clave> 'comando que usa $USR y $PASS'#{C::RESET}
|
||||
|
||||
#{C::CYAN}Ejemplos:#{C::RESET}
|
||||
#{C::GREEN}✓#{C::RESET} ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host'
|
||||
#{C::GREEN}✓#{C::RESET} ruby candados.rb run srv-ns8:root 'ssh $USR@$HOSTNAME "comando"'
|
||||
|
||||
#{C::BOLD}★ MÉTODO 2: load (Para shell interactivo)#{C::RESET}
|
||||
Carga USR y PASS en variables de entorno del shell actual.
|
||||
Usa un archivo temporal (600) que se auto-elimina tras el source.
|
||||
|
||||
#{C::DIM}eval $(ruby candados.rb load <clave>)#{C::RESET}
|
||||
|
||||
#{C::CYAN}Ejemplo:#{C::RESET}
|
||||
#{C::GREEN}✓#{C::RESET} eval $(ruby candados.rb load srv-dasu:rmonla)
|
||||
#{C::DIM} → Carga $USR y $PASS en tu shell actual#{C::RESET}
|
||||
#{C::GREEN}✓#{C::RESET} sshpass -p $PASS ssh $USR@servidor
|
||||
|
||||
#{C::YELLOW}━━━ Otros Comandos ━━━#{C::RESET}
|
||||
#{C::BOLD}sudo <comando>#{C::RESET} Ejecutar con sudo (resuelve contraseña automáticamente)
|
||||
#{C::BOLD}authorize#{C::RESET} Abrir candado (sesión de 30 min, requerida para operar)
|
||||
#{C::BOLD}cerrar#{C::RESET} Cerrar sesión manualmente antes de los 30 min
|
||||
#{C::BOLD}abrir#{C::RESET} Cargar clave SSH en ssh-add (usa passphrase de bóveda)
|
||||
|
||||
#{C::YELLOW}━━━ Gestión de Bóveda ━━━#{C::RESET}
|
||||
#{C::BOLD}set <clave>#{C::RESET} Guardar secreto (input seguro sin eco)
|
||||
#{C::BOLD}list#{C::RESET} Listar claves disponibles (no muestra valores)
|
||||
#{C::BOLD}rm <clave>#{C::RESET} Eliminar una clave de la bóveda
|
||||
|
||||
#{C::YELLOW}━━━ Utilidades ━━━#{C::RESET}
|
||||
abrir Autorizar SSH (cargar passphrase)
|
||||
encrypt <texto> Cifrar texto plano → token
|
||||
decrypt <token> Descifrar token → texto plano
|
||||
#{C::BOLD}encrypt <texto>#{C::RESET} Cifrar texto plano → token (para bóveda)
|
||||
#{C::BOLD}decrypt <token>#{C::RESET} Descifrar token → texto plano
|
||||
|
||||
#{C::YELLOW}Resolución automática de claves:#{C::RESET}
|
||||
#{C::DIM}• run/load: Detecta pares USR/PASS por convención:#{C::RESET}
|
||||
srv-dasu:rmonla → USR=rmonla, PASS=(valor cifrado)
|
||||
tailscale_dasuten → busca _user/_pass automáticamente
|
||||
#{C::DIM}• sudo: Resuelve la clave de sudo automáticamente:#{C::RESET}
|
||||
<hostname>:<usuario>:sudo (ej: ns8:rmonla:sudo)
|
||||
#{C::YELLOW}━━━ Resolución Automática de Claves ━━━#{C::RESET}
|
||||
#{C::DIM}• run/load con formato `servidor:usuario`:#{C::RESET}
|
||||
srv-dasu:rmonla → USR=rmonla, PASS=(descifrado de bóveda)
|
||||
#{C::DIM}• run/load con formato `prefijo_pass`:#{C::RESET}
|
||||
tailscale_dasuten_pass → busca automáticamente tailscale_dasuten_user
|
||||
#{C::DIM}• sudo resuelve la clave automáticamente:#{C::RESET}
|
||||
Busca: <hostname>:<usuario>:sudo → fallback: `sudo`
|
||||
|
||||
#{C::YELLOW}Ejemplos:#{C::RESET}
|
||||
#{C::DIM}# 1. Autorizar (dura 30 min)#{C::RESET}
|
||||
ruby candados.rb authorize
|
||||
#{C::YELLOW}━━━ Flujo de Trabajo Típico ━━━#{C::RESET}
|
||||
#{C::DIM}1. Autorizar (una vez cada 30 min):#{C::RESET}
|
||||
ruby candados.rb authorize
|
||||
|
||||
#{C::GREEN}# 2. Ejecutar comando con credenciales (RECOMENDADO)#{C::RESET}
|
||||
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host'
|
||||
ruby candados.rb run admindasu SSHPASS 'sshpass -e ssh root@host'
|
||||
#{C::DIM}2. Ejecutar comandos (Método recomendado):#{C::RESET}
|
||||
ruby candados.rb run srv-ns8:rmonla 'sshpass -p $PASS ssh $USR@host "comando"'
|
||||
|
||||
#{C::DIM}# 3. Ejecutar con sudo#{C::RESET}
|
||||
ruby candados.rb sudo tailscale set --operator=$USER
|
||||
#{C::DIM}3. O cargar en shell interactivo:#{C::RESET}
|
||||
eval $(ruby candados.rb load srv-ns8:rmonla)
|
||||
sshpass -p $PASS ssh $USR@host
|
||||
|
||||
#{C::DIM}# 4. Cargar en shell interactivo (secundario)#{C::RESET}
|
||||
eval $(ruby candados.rb load srv-dasu:rmonla)
|
||||
#{C::RED}⚠️ ADVERTENCIAS DE SEGURIDAD:#{C::RESET}
|
||||
#{C::BOLD}• NUNCA usar `get`#{C::RESET} en automatizaciones → imprime la contraseña a stdout
|
||||
#{C::BOLD}• `run` y `sudo`#{C::RESET} → las credenciales no dejan rastro en logs
|
||||
#{C::BOLD}• `load`#{C::RESET} → archivo temporal con permisos 600, auto-borrado
|
||||
#{C::BOLD}• Siempre ejecutar `authorize`#{C::RESET} antes de operar (sesión de 30 min)
|
||||
|
||||
#{C::RED}⚠ Seguridad:#{C::RESET}
|
||||
#{C::DIM}• run/sudo: Las credenciales NUNCA se imprimen en pantalla ni logs.#{C::RESET}
|
||||
#{C::DIM}• load: Usa archivo temporal (600) con autoborrado, nada visible.#{C::RESET}
|
||||
#{C::DIM}• get: ⚠ Imprime el valor a stdout. Usar solo si es necesario.#{C::RESET}
|
||||
#{C::DIM}Documentación completa: adn/tools/candados/README.md#{C::RESET}
|
||||
HELP
|
||||
end
|
||||
|
||||
alias cmd_help cmd_help_completo
|
||||
|
||||
# ─── Recordatorio de Seguridad (solo en ayuda) ──────────────────
|
||||
def mostrar_recordatorio_seguridad
|
||||
puts <<~RECORDATORIO
|
||||
#{C::CYAN}╔════════════════════════════════════════════════════════════════╗#{C::RESET}
|
||||
#{C::CYAN}║#{C::RESET} 🔐 #{C::BOLD}REGLA DE ORO: NUNCA MOSTRAR CONTRASEÑAS#{C::RESET} #{C::CYAN}║#{C::RESET}
|
||||
#{C::CYAN}║#{C::RESET} #{C::GREEN}✓ CORRECTO:#{C::RESET} ruby candados.rb run <clave> '<cmd>' #{C::CYAN}║#{C::RESET}
|
||||
#{C::CYAN}║#{C::RESET} #{C::RED}✗ INCORRECTO:#{C::RESET} ruby candados.rb get <clave> #{C::CYAN}║#{C::RESET}
|
||||
#{C::CYAN}║#{C::RESET} → `run` inyecta USR/PASS sin mostrar. `get` imprime y deja rastro. #{C::CYAN}║#{C::RESET}
|
||||
#{C::CYAN}╚════════════════════════════════════════════════════════════════╝#{C::RESET}
|
||||
RECORDATORIO
|
||||
puts
|
||||
end
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────────────
|
||||
if __FILE__ == $0
|
||||
comando = ARGV[0]
|
||||
@@ -496,6 +633,10 @@ if __FILE__ == $0
|
||||
cripto = Cripto.new(cargar_master_key)
|
||||
boveda = Boveda.new(BOVEDA)
|
||||
cmd_sudo(cripto, boveda, ARGV[1..])
|
||||
when 'ssh'
|
||||
cripto = Cripto.new(cargar_master_key)
|
||||
boveda = Boveda.new(BOVEDA)
|
||||
cmd_ssh(cripto, boveda, ARGV[1], ARGV[2], *ARGV[3..])
|
||||
when 'encrypt', 'decrypt', 'get', 'load', 'run', 'set', 'rm', 'list'
|
||||
cripto = Cripto.new(cargar_master_key)
|
||||
boveda = Boveda.new(BOVEDA)
|
||||
@@ -509,11 +650,14 @@ if __FILE__ == $0
|
||||
when 'rm' then cmd_rm(boveda, arg)
|
||||
when 'list' then cmd_list(boveda)
|
||||
end
|
||||
when 'help', 'ayuda', '-h', '--help', nil
|
||||
cmd_help
|
||||
when '--help', '-h'
|
||||
cmd_help_completo
|
||||
when 'help', 'ayuda', nil
|
||||
cmd_help_resumida
|
||||
else
|
||||
$stderr.puts "#{C::RED}✗ Comando desconocido: '#{comando}'#{C::RESET}"
|
||||
cmd_help
|
||||
$stderr.puts ""
|
||||
cmd_help_resumida
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,6 +66,7 @@ module Dron
|
||||
lines << "\n#{Color::BOLD}Opciones:#{Color::RESET}"
|
||||
lines << " #{Color::GREEN}--nota \"texto\"#{Color::RESET} Nota descriptiva para la bitácora"
|
||||
lines << " #{Color::GREEN}--evento ID#{Color::RESET} Vincular a evento existente"
|
||||
lines << " #{Color::GREEN}--nodo NOMBRE#{Color::RESET} Nodo para bitácora (auto-detect si no se especifica)"
|
||||
lines << " #{Color::GREEN}--timeout N#{Color::RESET} Timeout en segundos (default: 3600)"
|
||||
lines << " #{Color::GREEN}--dry-run#{Color::RESET} Simulación sin ejecutar"
|
||||
lines << " #{Color::GREEN}--help#{Color::RESET} Mostrar esta ayuda"
|
||||
@@ -107,7 +108,8 @@ module Dron
|
||||
nota: opciones[:nota],
|
||||
evento_id: opciones[:evento_id],
|
||||
timeout: opciones[:timeout],
|
||||
flujo_id: opciones[:flujo_id]
|
||||
flujo_id: opciones[:flujo_id],
|
||||
nodo: opciones[:nodo]
|
||||
)
|
||||
|
||||
# Mostrar resultado
|
||||
@@ -128,6 +130,7 @@ module Dron
|
||||
evento_id: nil,
|
||||
timeout: 3600,
|
||||
flujo_id: nil,
|
||||
nodo: nil,
|
||||
cmd: nil
|
||||
}
|
||||
|
||||
@@ -143,6 +146,8 @@ module Dron
|
||||
opciones[:timeout] = args.shift.to_i
|
||||
when '--flujo'
|
||||
opciones[:flujo_id] = args.shift
|
||||
when '--nodo'
|
||||
opciones[:nodo] = args.shift
|
||||
when '--help', '-h'
|
||||
Dispatcher.mostrar_ayuda
|
||||
exit 0
|
||||
|
||||
@@ -13,15 +13,15 @@ module Dron
|
||||
HEARTBEAT_INTERVAL = 30
|
||||
|
||||
class << self
|
||||
def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil)
|
||||
def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil)
|
||||
Base.logger.info("🛠️ Dron Ejecutor iniciado: #{Process.pid}")
|
||||
dron_id = ADN::DB::DronDB.generar_dron_id
|
||||
|
||||
ADN::DB::DronDB.registrar_inicio(tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, metadata: { nota: nota, evento_id: evento_id, timeout: timeout })
|
||||
evento_id = registrar_bitacora(nota, evento_id, 'inicio') if nota
|
||||
ADN::DB::DronDB.registrar_inicio(tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, metadata: { nota: nota, evento_id: evento_id, timeout: timeout, nodo: nodo })
|
||||
evento_id = registrar_bitacora(nota, evento_id, 'inicio', nodo) if nota
|
||||
resultado = ejecutar_comando(cmd, dron_id, timeout)
|
||||
ADN::DB::DronDB.registrar_fin(dron_id, resultado[:exit_code], resultado[:output])
|
||||
registrar_bitacora(nota, evento_id, 'fin', resultado) if evento_id
|
||||
registrar_bitacora(nota, evento_id, 'fin', resultado, nodo) if evento_id
|
||||
|
||||
Base.logger.info("🛠️ Dron Ejecutor completado: #{dron_id} - #{resultado[:exit_code] == 0 ? '✅' : '❌'}")
|
||||
{ dron_id: dron_id, exit_code: resultado[:exit_code], duration: resultado[:duration], evento_id: evento_id }
|
||||
@@ -67,14 +67,18 @@ module Dron
|
||||
{ exit_code: exit_code || 1, output: output.join("\n").strip, duration: (Time.now - start_time).round(2) }
|
||||
end
|
||||
|
||||
def registrar_bitacora(nota, evento_id, tipo, resultado = nil)
|
||||
def registrar_bitacora(nota, evento_id, tipo, resultado = nil, nodo = nil)
|
||||
return nil unless nota
|
||||
require_relative '../../db/core/bitacora_db'
|
||||
|
||||
# Detectar nodo automáticamente si no se especifica
|
||||
nodo_id = detectar_nodo_id(nodo)
|
||||
|
||||
if tipo == 'inicio'
|
||||
descripcion = "🛸 dron: #{nota}"
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
db.crear_evento(nodo_id: 1, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳')
|
||||
evento = db.crear_evento(nodo_id: nodo_id, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳')
|
||||
evento['id'] # Retornar ID para actualizar después
|
||||
end
|
||||
else
|
||||
estado = resultado[:exit_code] == 0 ? '✅' : '❌'
|
||||
@@ -82,11 +86,44 @@ module Dron
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
db.actualizar_evento(evento_id, descripcion: descripcion, fin: Time.now.strftime('%H:%M'))
|
||||
end
|
||||
evento_id # Retornar mismo ID
|
||||
end
|
||||
rescue StandardError => e
|
||||
Base.logger.warn("⚠️ Error en bitácora: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
def detectar_nodo_id(nodo)
|
||||
return nodo if nodo.is_a?(Integer)
|
||||
|
||||
# Si no se especifica nodo, usar el hostname para detectar
|
||||
require 'socket'
|
||||
hostname = Socket.gethostname.downcase
|
||||
|
||||
# Buscar nodo por nombre en la tabla de nodos
|
||||
require_relative '../../db/core/bitacora_db'
|
||||
sql = 'SELECT id FROM nodos WHERE LOWER(nombre) = $1 LIMIT 1'
|
||||
|
||||
resultado = nil
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
resultado = db.execute(sql, [hostname])
|
||||
end
|
||||
|
||||
if resultado&.any?
|
||||
resultado.first['id']
|
||||
else
|
||||
# Fallback: buscar por prefijo (ej: srv-ns8 coincide con srv-ns8.frlr.utn.edu.ar)
|
||||
sql = 'SELECT id FROM nodos WHERE LOWER(nombre) LIKE $1 LIMIT 1'
|
||||
resultado = nil
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
resultado = db.execute(sql, ["#{hostname}%"])
|
||||
end
|
||||
resultado&.any? ? resultado.first['id'] : 1
|
||||
end
|
||||
rescue StandardError => e
|
||||
Base.logger.warn("⚠️ Error detectando nodo: #{e.message}, usando default (1)")
|
||||
1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -666,20 +666,45 @@ module BitacorasDB
|
||||
|
||||
where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}"
|
||||
|
||||
# Para events, adaptar condiciones (solo las que aplican)
|
||||
where_clause_events = if conditions.empty?
|
||||
''
|
||||
else
|
||||
# Reemplazar referencias a e. por ev. y eliminar condiciones que no aplican a events
|
||||
conditions_events = conditions.reject { |c| c.include?('bitacora_id') || c.include?('tema_id') || c.include?('ambito_id') }
|
||||
"WHERE #{conditions_events.join(' AND ').gsub('e.', 'ev.')}"
|
||||
end
|
||||
|
||||
# Unificar eventos de tabla events y entradas para mostrar todo junto
|
||||
query = <<~SQL
|
||||
SELECT e.id, e.inicio, e.fin, e.descripcion, e.estado, e.modo, e.es_ia,
|
||||
e.tema_id, e.bitacora_id, e.nodo_id, e.ambito_id, e.created_at, e.updated_at,
|
||||
SELECT ev.id, ev.inicio::text as inicio, ev.fin::text as fin, ev.descripcion, ev.estado,
|
||||
'A' as modo, false as es_ia,
|
||||
NULL as tema_id, NULL as bitacora_id, ev.nodo_id, NULL as ambito_id,
|
||||
ev.created_at, ev.updated_at,
|
||||
CURRENT_DATE as bitacora_fecha,
|
||||
n.nombre as nodo_nombre,
|
||||
NULL as tema_titulo,
|
||||
na.nombre as ambito_nombre
|
||||
FROM bitacoras.events ev
|
||||
LEFT JOIN bitacoras.nodos n ON ev.nodo_id = n.id
|
||||
LEFT JOIN bitacoras.ambitos na ON n.ambito_id = na.id
|
||||
#{where_clause_events}
|
||||
UNION ALL
|
||||
SELECT e.id, e.inicio::text, e.fin::text, e.descripcion, e.estado, e.modo, e.es_ia,
|
||||
e.tema_id, e.bitacora_id, e.nodo_id, e.ambito_id,
|
||||
e.created_at, e.updated_at,
|
||||
b.fecha as bitacora_fecha,
|
||||
n.nombre as nodo_nombre,
|
||||
t.titulo as tema_titulo,
|
||||
a.nombre as ambito_nombre
|
||||
COALESCE(a.nombre, na.nombre) as ambito_nombre
|
||||
FROM #{SCHEMA}.#{TABLES[:entradas]} e
|
||||
LEFT JOIN #{SCHEMA}.#{TABLES[:bitacoras]} b ON e.bitacora_id = b.id
|
||||
LEFT JOIN #{SCHEMA}.#{TABLES[:nodos]} n ON e.nodo_id = n.id
|
||||
LEFT JOIN #{SCHEMA}.#{TABLES[:temas]} t ON e.tema_id = t.id
|
||||
LEFT JOIN #{SCHEMA}.ambitos a ON e.ambito_id = a.id
|
||||
LEFT JOIN #{SCHEMA}.ambitos na ON n.ambito_id = na.id
|
||||
#{where_clause}
|
||||
ORDER BY COALESCE(b.fecha, CURRENT_DATE) DESC, e.inicio DESC
|
||||
ORDER BY bitacora_fecha DESC, inicio DESC
|
||||
LIMIT $#{param_index} OFFSET $#{param_index + 1}
|
||||
SQL
|
||||
|
||||
@@ -1007,6 +1032,7 @@ module BitacorasDB
|
||||
public
|
||||
|
||||
# Crear un evento en la tabla events (visible en Bitácora Web)
|
||||
# El ámbito se obtiene automáticamente del nodo mediante JOIN en las consultas
|
||||
def crear_evento(nodo_id:, descripcion:, estado: '⏳', inicio: nil, fin: nil, modo: 'A', metadata: nil)
|
||||
inicio ||= Time.now.strftime('%H:%M')
|
||||
query = <<~SQL
|
||||
|
||||
Reference in New Issue
Block a user