feat: funciones pass para gestión de secretos — get, set, list, delete, generate, sync
Wrappers Bash sobre pass (password-store) para CRUD de secretos, generación de contraseñas y sincronización con git. Incluye script de test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: pass_delete
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pass_delete(entry: string) -> void"
|
||||
description: "Elimina un secreto del password store (pass)."
|
||||
tags: [pass, secret, credential, delete]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["elimina entrada de test", "falla con entrada inexistente"]
|
||||
test_file_path: "bash/functions/infra/pass_test.sh"
|
||||
file_path: "bash/functions/infra/pass_delete.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source pass_delete.sh
|
||||
pass_delete agentes/viejo-token
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `pass rm -f` para eliminar sin prompt de confirmacion.
|
||||
@@ -0,0 +1,22 @@
|
||||
# pass_delete
|
||||
# -----------
|
||||
# Elimina un secreto del password store.
|
||||
# Sale con exit code 1 si la entrada no existe o pass falla.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source pass_delete.sh
|
||||
# pass_delete agentes/viejo-token
|
||||
|
||||
pass_delete() {
|
||||
local entry="$1"
|
||||
|
||||
if [ -z "$entry" ]; then
|
||||
echo "pass_delete: se requiere nombre de entrada" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! pass rm -f "$entry" >/dev/null 2>&1; then
|
||||
echo "pass_delete: fallo al eliminar '$entry'" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: pass_generate
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pass_generate(entry: string, [length: int]) -> string"
|
||||
description: "Genera un password aleatorio, lo almacena en el password store e imprime el valor generado."
|
||||
tags: [pass, secret, credential, generate, random]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["genera password de longitud especifica", "default 24 chars"]
|
||||
test_file_path: "bash/functions/infra/pass_test.sh"
|
||||
file_path: "bash/functions/infra/pass_generate.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source pass_generate.sh
|
||||
new_pass=$(pass_generate agentes/nuevo-servicio 32)
|
||||
echo "password generado: $new_pass"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `pass generate -f -n` (force overwrite, no symbols). Default 24 caracteres alfanumericos.
|
||||
@@ -0,0 +1,31 @@
|
||||
# pass_generate
|
||||
# -------------
|
||||
# Genera un password aleatorio y lo almacena en el password store.
|
||||
# Imprime el password generado a stdout.
|
||||
# Sale con exit code 1 si pass falla.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source pass_generate.sh
|
||||
# pass_generate agentes/nuevo-token 32
|
||||
# pass_generate agentes/api-key # default 24 chars
|
||||
|
||||
pass_generate() {
|
||||
local entry="$1"
|
||||
local length="${2:-24}"
|
||||
|
||||
if [ -z "$entry" ]; then
|
||||
echo "pass_generate: se requiere nombre de entrada" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local output
|
||||
output=$(pass generate -f -n "$entry" "$length" 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "pass_generate: fallo al generar '$entry': $output" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# pass generate imprime ANSI escape codes + header + password
|
||||
# Extraer ultima linea y limpiar escape codes
|
||||
echo "$output" | tail -1 | sed 's/\x1b\[[0-9;]*m//g'
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: pass_get
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pass_get(entry: string) -> string"
|
||||
description: "Lee un secreto del password store (pass) y lo imprime a stdout."
|
||||
tags: [pass, secret, credential, get]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["lee entrada existente", "falla con entrada inexistente"]
|
||||
test_file_path: "bash/functions/infra/pass_test.sh"
|
||||
file_path: "bash/functions/infra/pass_get.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source pass_get.sh
|
||||
token=$(pass_get agentes/dataforge-token)
|
||||
export GITEA_TOKEN="$token"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `pass show` internamente. Requiere GPG key desbloqueada. No imprime newline final (usa printf %s).
|
||||
@@ -0,0 +1,26 @@
|
||||
# pass_get
|
||||
# --------
|
||||
# Lee un secreto del password store y lo imprime a stdout.
|
||||
# Sale con exit code 1 si la entrada no existe o pass falla.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source pass_get.sh
|
||||
# token=$(pass_get agentes/dataforge-token)
|
||||
|
||||
pass_get() {
|
||||
local entry="$1"
|
||||
|
||||
if [ -z "$entry" ]; then
|
||||
echo "pass_get: se requiere nombre de entrada" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local value
|
||||
value=$(pass show "$entry" 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "pass_get: no se pudo leer '$entry'" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "$value"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: pass_list
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pass_list([prefix: string]) -> json"
|
||||
description: "Lista entradas del password store como JSON array. Filtra opcionalmente por prefijo."
|
||||
tags: [pass, secret, credential, list]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["lista todas las entradas", "filtra por prefijo"]
|
||||
test_file_path: "bash/functions/infra/pass_test.sh"
|
||||
file_path: "bash/functions/infra/pass_list.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source pass_list.sh
|
||||
entries=$(pass_list agentes)
|
||||
# ["dataforge-token","egutierrez-token","gitea-url"]
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Parsea el output tree de `pass ls` y lo convierte a JSON array. Cada entrada es un string con el nombre relativo al prefijo.
|
||||
@@ -0,0 +1,40 @@
|
||||
# pass_list
|
||||
# ---------
|
||||
# Lista entradas del password store como JSON array.
|
||||
# Opcionalmente filtra por prefijo.
|
||||
# Sale con exit code 1 si pass falla.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source pass_list.sh
|
||||
# pass_list # todas las entradas
|
||||
# pass_list agentes # solo bajo agentes/
|
||||
|
||||
pass_list() {
|
||||
local prefix="${1:-}"
|
||||
|
||||
local raw
|
||||
raw=$(pass ls "$prefix" 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "pass_list: fallo al listar entradas" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parsear output de pass: extraer nombres limpios (sin tree chars)
|
||||
local entries
|
||||
entries=$(echo "$raw" | sed 's/[│├└──── ]//g' | sed '/^$/d' | grep -v '^Password' | grep -v '^[[:space:]]*$')
|
||||
|
||||
# Construir JSON array
|
||||
printf '['
|
||||
local first=true
|
||||
while IFS= read -r line; do
|
||||
line=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
|
||||
[ -z "$line" ] && continue
|
||||
if [ "$first" = true ]; then
|
||||
first=false
|
||||
else
|
||||
printf ','
|
||||
fi
|
||||
printf '"%s"' "$line"
|
||||
done <<< "$entries"
|
||||
printf ']'
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: pass_set
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pass_set(entry: string, [value: string]) -> void"
|
||||
description: "Inserta o sobreescribe un secreto en el password store (pass)."
|
||||
tags: [pass, secret, credential, set, insert]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["inserta valor y lo lee de vuelta", "sobreescribe valor existente"]
|
||||
test_file_path: "bash/functions/infra/pass_test.sh"
|
||||
file_path: "bash/functions/infra/pass_set.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source pass_set.sh
|
||||
pass_set agentes/nuevo-servicio "token-abc123"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `pass insert -m -f` para forzar sobreescritura sin prompt interactivo. Si no se pasa valor como argumento, lee de stdin.
|
||||
@@ -0,0 +1,31 @@
|
||||
# pass_set
|
||||
# --------
|
||||
# Inserta o sobreescribe un secreto en el password store.
|
||||
# Lee el valor de stdin si no se pasa como segundo argumento.
|
||||
# Sale con exit code 1 si pass falla.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source pass_set.sh
|
||||
# pass_set agentes/nuevo-token "mi-token-secreto"
|
||||
# echo "mi-token" | pass_set agentes/nuevo-token
|
||||
|
||||
pass_set() {
|
||||
local entry="$1"
|
||||
local value="$2"
|
||||
|
||||
if [ -z "$entry" ]; then
|
||||
echo "pass_set: se requiere nombre de entrada" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s' "$value" | pass insert -m -f "$entry" >/dev/null 2>&1
|
||||
else
|
||||
pass insert -m -f "$entry" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "pass_set: fallo al escribir '$entry'" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: pass_sync
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pass_sync() -> json"
|
||||
description: "Sincroniza el password store con el repositorio git remoto (pull + push)."
|
||||
tags: [pass, secret, sync, git]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["sincroniza con remoto"]
|
||||
test_file_path: "bash/functions/infra/pass_test.sh"
|
||||
file_path: "bash/functions/infra/pass_sync.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source pass_sync.sh
|
||||
result=$(pass_sync)
|
||||
# {"pull":"Already up to date.","push":"Everything up-to-date"}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Ejecuta `pass git pull` seguido de `pass git push`. Requiere que el password store tenga un remote git configurado. Retorna JSON con la ultima linea de cada operacion.
|
||||
@@ -0,0 +1,28 @@
|
||||
# pass_sync
|
||||
# ---------
|
||||
# Sincroniza el password store con el repositorio git remoto (pull + push).
|
||||
# Sale con exit code 1 si la sincronizacion falla.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source pass_sync.sh
|
||||
# pass_sync
|
||||
|
||||
pass_sync() {
|
||||
local pull_out
|
||||
pull_out=$(pass git pull 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "pass_sync: fallo en git pull: $pull_out" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local push_out
|
||||
push_out=$(pass git push 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "pass_sync: fallo en git push: $push_out" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '{"pull":"%s","push":"%s"}' \
|
||||
"$(echo "$pull_out" | tail -1 | sed 's/"/\\"/g')" \
|
||||
"$(echo "$push_out" | tail -1 | sed 's/"/\\"/g')"
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
# pass_test.sh — Tests para funciones pass del registry
|
||||
# Usa la entrada test/fn_registry_test como sandbox (se limpia al final)
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/pass_get.sh"
|
||||
source "$SCRIPT_DIR/pass_set.sh"
|
||||
source "$SCRIPT_DIR/pass_list.sh"
|
||||
source "$SCRIPT_DIR/pass_delete.sh"
|
||||
source "$SCRIPT_DIR/pass_generate.sh"
|
||||
source "$SCRIPT_DIR/pass_sync.sh"
|
||||
|
||||
TEST_ENTRY="test/fn_registry_test"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass_cleanup() {
|
||||
pass rm -f "$TEST_ENTRY" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local test_name="$1" got="$2" want="$3"
|
||||
if [ "$got" = "$want" ]; then
|
||||
echo " PASS: $test_name"
|
||||
((PASS++))
|
||||
else
|
||||
echo " FAIL: $test_name (got='$got', want='$want')"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" got="$2" want="$3"
|
||||
if echo "$got" | grep -q "$want"; then
|
||||
echo " PASS: $test_name"
|
||||
((PASS++))
|
||||
else
|
||||
echo " FAIL: $test_name (got='$got', want contener '$want')"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_nonzero() {
|
||||
local test_name="$1" got="$2"
|
||||
if [ -n "$got" ]; then
|
||||
echo " PASS: $test_name"
|
||||
((PASS++))
|
||||
else
|
||||
echo " FAIL: $test_name (got vacio)"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_fail() {
|
||||
local test_name="$1"
|
||||
shift
|
||||
set +e
|
||||
"$@" 2>/dev/null
|
||||
local rc=$?
|
||||
set -e
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo " FAIL: $test_name (esperaba fallo pero exitoso)"
|
||||
((FAIL++))
|
||||
else
|
||||
echo " PASS: $test_name"
|
||||
((PASS++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Pre-check
|
||||
if ! command -v pass &>/dev/null; then
|
||||
echo "SKIP: pass no disponible"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
trap pass_cleanup EXIT
|
||||
|
||||
echo "=== pass_get ==="
|
||||
|
||||
echo " test: lee entrada existente (agentes/gitea-url)"
|
||||
got=$(pass_get agentes/gitea-url)
|
||||
assert_nonzero "lee entrada existente" "$got"
|
||||
|
||||
echo " test: falla con entrada inexistente"
|
||||
assert_fail "falla con entrada inexistente" pass_get "no/existe/xyz"
|
||||
|
||||
echo ""
|
||||
echo "=== pass_set ==="
|
||||
|
||||
echo " test: inserta valor y lo lee de vuelta"
|
||||
pass_set "$TEST_ENTRY" "test-value-12345"
|
||||
got=$(pass_get "$TEST_ENTRY")
|
||||
assert_eq "inserta y lee" "$got" "test-value-12345"
|
||||
|
||||
echo " test: sobreescribe valor existente"
|
||||
pass_set "$TEST_ENTRY" "overwritten-value"
|
||||
got=$(pass_get "$TEST_ENTRY")
|
||||
assert_eq "sobreescribe" "$got" "overwritten-value"
|
||||
|
||||
# Limpiar para siguiente test
|
||||
pass_cleanup
|
||||
|
||||
echo ""
|
||||
echo "=== pass_list ==="
|
||||
|
||||
echo " test: lista todas las entradas"
|
||||
got=$(pass_list)
|
||||
assert_contains "lista todas" "$got" "dataforge-token"
|
||||
|
||||
echo " test: filtra por prefijo agentes"
|
||||
got=$(pass_list agentes)
|
||||
assert_contains "filtra agentes" "$got" "gitea-url"
|
||||
|
||||
echo ""
|
||||
echo "=== pass_generate ==="
|
||||
|
||||
echo " test: genera password de 16 chars"
|
||||
generated=$(pass_generate "$TEST_ENTRY" 16)
|
||||
assert_eq "longitud 16" "${#generated}" "16"
|
||||
|
||||
echo " test: valor almacenado coincide"
|
||||
stored=$(pass_get "$TEST_ENTRY")
|
||||
assert_eq "stored = generated" "$stored" "$generated"
|
||||
|
||||
pass_cleanup
|
||||
|
||||
echo " test: default 24 chars"
|
||||
generated=$(pass_generate "$TEST_ENTRY")
|
||||
assert_eq "longitud default 24" "${#generated}" "24"
|
||||
|
||||
pass_cleanup
|
||||
|
||||
echo ""
|
||||
echo "=== pass_delete ==="
|
||||
|
||||
echo " test: elimina entrada de test"
|
||||
pass_set "$TEST_ENTRY" "to-delete"
|
||||
pass_delete "$TEST_ENTRY"
|
||||
assert_fail "despues de delete no se puede leer" pass_get "$TEST_ENTRY"
|
||||
|
||||
echo " test: falla con entrada inexistente"
|
||||
assert_fail "delete inexistente" pass_delete "no/existe/xyz_delete_test"
|
||||
|
||||
echo ""
|
||||
echo "=== pass_sync ==="
|
||||
|
||||
echo " test: sincroniza con remoto"
|
||||
got=$(pass_sync)
|
||||
assert_contains "sync retorna json" "$got" "pull"
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Resultados: $PASS passed, $FAIL failed"
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user