From b698177860ed9d4938d999f5aea5f2766bba24a3 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Thu, 2 Apr 2026 22:03:44 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20funciones=20pass=20para=20gesti=C3=B3n?= =?UTF-8?q?=20de=20secretos=20=E2=80=94=20get,=20set,=20list,=20delete,=20?= =?UTF-8?q?generate,=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- bash/functions/infra/pass_delete.md | 32 ++++++ bash/functions/infra/pass_delete.sh | 22 ++++ bash/functions/infra/pass_generate.md | 33 ++++++ bash/functions/infra/pass_generate.sh | 31 +++++ bash/functions/infra/pass_get.md | 33 ++++++ bash/functions/infra/pass_get.sh | 26 +++++ bash/functions/infra/pass_list.md | 33 ++++++ bash/functions/infra/pass_list.sh | 40 +++++++ bash/functions/infra/pass_set.md | 32 ++++++ bash/functions/infra/pass_set.sh | 31 +++++ bash/functions/infra/pass_sync.md | 33 ++++++ bash/functions/infra/pass_sync.sh | 28 +++++ bash/functions/infra/pass_test.sh | 157 ++++++++++++++++++++++++++ 13 files changed, 531 insertions(+) create mode 100644 bash/functions/infra/pass_delete.md create mode 100644 bash/functions/infra/pass_delete.sh create mode 100644 bash/functions/infra/pass_generate.md create mode 100644 bash/functions/infra/pass_generate.sh create mode 100644 bash/functions/infra/pass_get.md create mode 100644 bash/functions/infra/pass_get.sh create mode 100644 bash/functions/infra/pass_list.md create mode 100644 bash/functions/infra/pass_list.sh create mode 100644 bash/functions/infra/pass_set.md create mode 100644 bash/functions/infra/pass_set.sh create mode 100644 bash/functions/infra/pass_sync.md create mode 100644 bash/functions/infra/pass_sync.sh create mode 100644 bash/functions/infra/pass_test.sh diff --git a/bash/functions/infra/pass_delete.md b/bash/functions/infra/pass_delete.md new file mode 100644 index 00000000..fc6d6dae --- /dev/null +++ b/bash/functions/infra/pass_delete.md @@ -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. diff --git a/bash/functions/infra/pass_delete.sh b/bash/functions/infra/pass_delete.sh new file mode 100644 index 00000000..64b8231a --- /dev/null +++ b/bash/functions/infra/pass_delete.sh @@ -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 +} diff --git a/bash/functions/infra/pass_generate.md b/bash/functions/infra/pass_generate.md new file mode 100644 index 00000000..c10b314e --- /dev/null +++ b/bash/functions/infra/pass_generate.md @@ -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. diff --git a/bash/functions/infra/pass_generate.sh b/bash/functions/infra/pass_generate.sh new file mode 100644 index 00000000..58d0af2e --- /dev/null +++ b/bash/functions/infra/pass_generate.sh @@ -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' +} diff --git a/bash/functions/infra/pass_get.md b/bash/functions/infra/pass_get.md new file mode 100644 index 00000000..36c2755d --- /dev/null +++ b/bash/functions/infra/pass_get.md @@ -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). diff --git a/bash/functions/infra/pass_get.sh b/bash/functions/infra/pass_get.sh new file mode 100644 index 00000000..b6a2dcea --- /dev/null +++ b/bash/functions/infra/pass_get.sh @@ -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" +} diff --git a/bash/functions/infra/pass_list.md b/bash/functions/infra/pass_list.md new file mode 100644 index 00000000..461e1bdb --- /dev/null +++ b/bash/functions/infra/pass_list.md @@ -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. diff --git a/bash/functions/infra/pass_list.sh b/bash/functions/infra/pass_list.sh new file mode 100644 index 00000000..09b24a94 --- /dev/null +++ b/bash/functions/infra/pass_list.sh @@ -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 ']' +} diff --git a/bash/functions/infra/pass_set.md b/bash/functions/infra/pass_set.md new file mode 100644 index 00000000..9b2ea70c --- /dev/null +++ b/bash/functions/infra/pass_set.md @@ -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. diff --git a/bash/functions/infra/pass_set.sh b/bash/functions/infra/pass_set.sh new file mode 100644 index 00000000..a20c2e96 --- /dev/null +++ b/bash/functions/infra/pass_set.sh @@ -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 +} diff --git a/bash/functions/infra/pass_sync.md b/bash/functions/infra/pass_sync.md new file mode 100644 index 00000000..4a2d62e2 --- /dev/null +++ b/bash/functions/infra/pass_sync.md @@ -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. diff --git a/bash/functions/infra/pass_sync.sh b/bash/functions/infra/pass_sync.sh new file mode 100644 index 00000000..b25cfcb6 --- /dev/null +++ b/bash/functions/infra/pass_sync.sh @@ -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')" +} diff --git a/bash/functions/infra/pass_test.sh b/bash/functions/infra/pass_test.sh new file mode 100644 index 00000000..94d6c483 --- /dev/null +++ b/bash/functions/infra/pass_test.sh @@ -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