Connect local SXCHNGE to the Sendico stand

This commit was merged in pull request #53.
This commit is contained in:
Stephan D committed 2026-10-16 10:19:59 +02:00
1 parent 34d8ef18d1
commit a5689876ff
14 files changed
+336 -9

No files matched your search

+11 -4
View File
@@ -54,24 +54,31 @@ provision_mongo() {
{ cat "$WORK/admin.js"; cat infra/local/mongo/mongo-client.js; } | mongo >/dev/null || fail 'Browser client provisioning failed'
}
provision_agents() {
for service in bff notifications nats; do
services='bff notifications nats'
if [ -f "$STATE/sendico/settings.env" ]; then services="$services sendico"; fi
for service in $services; do
directory="$STATE/agents/$service"
mkdir -p "$directory"; chmod 755 "$directory"
case "$service" in
bff) paths='services/bff/jwt';;
notifications) paths='infrastructure/nats/auth services/notifications/email/resend';;
nats) paths=infrastructure/nats/auth;;
sendico) paths=services/bff/sendico;;
esac
: > "$WORK/policy"
for path in $paths; do printf 'path "kv/data/%s" { capabilities = ["read"] }\n' "$path" >> "$WORK/policy"; done
if [ "$service" != nats ]; then printf 'path "database/creds/%s" { capabilities = ["read"] }\n' "$service" >> "$WORK/policy"; fi
case "$service" in bff|notifications) printf 'path "database/creds/%s" { capabilities = ["read"] }\n' "$service" >> "$WORK/policy";; esac
jq -n --rawfile policy "$WORK/policy" '{policy:$policy}' | vault "sys/policies/acl/$service" POST >/dev/null
jq -n --arg service "$service" '{token_policies:[$service],token_period:"1h",secret_id_ttl:"0",secret_id_num_uses:0}' | vault "auth/approle/role/$service" POST >/dev/null
if [ "$service" = sendico ]; then
printf '{"token_policies":["sendico"],"token_ttl":"5m","token_max_ttl":"5m","secret_id_ttl":"0","secret_id_num_uses":0}' | vault "auth/approle/role/$service" POST >/dev/null
else
jq -n --arg service "$service" '{token_policies:[$service],token_period:"1h",secret_id_ttl:"0",secret_id_num_uses:0}' | vault "auth/approle/role/$service" POST >/dev/null
fi
vault "auth/approle/role/$service/role-id" | jq -r '.data.role_id' | write "$directory/role-id" 444
if [ ! -f "$directory/secret-id" ]; then
printf '{}' | vault "auth/approle/role/$service/secret-id" POST | jq -r '.data.secret_id' | write "$directory/secret-id" 444
fi
if ! cmp -s "infra/local/vault/$service.hcl" "$directory/config.hcl"; then
if [ "$service" != sendico ] && ! cmp -s "infra/local/vault/$service.hcl" "$directory/config.hcl"; then
cat "infra/local/vault/$service.hcl" | write "$directory/config.hcl" 444
if compose ps --status running --services "$service" | grep -qx "$service"; then
compose up -d --no-deps --force-recreate --wait --wait-timeout 120 "$service"
+7 -1
View File
@@ -14,7 +14,13 @@ trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
fail() { echo "$*" >&2; exit 1; }
compose() { docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"; }
compose() {
if [ -f "$STATE/sendico/settings.env" ]; then
docker compose --project-directory . -f infra/local/compose.yml -f infra/local/sendico/compose.yml --env-file "$STATE/images.env" "$@"
else
docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"
fi
}
write() {
target=$1; mode=${2:-600}
mkdir -p "$(dirname "$target")"
+1
View File
@@ -7,6 +7,7 @@ case "${1:-}" in
images) . infra/local/scripts/images.sh; images;;
up) . infra/local/scripts/bootstrap.sh; up;;
email) . infra/local/scripts/bootstrap.sh; email;;
sendico) . infra/local/scripts/bootstrap.sh; . infra/local/scripts/sendico.sh; configure_sendico;;
verify|verify-infra) . infra/local/scripts/verify.sh; existing; infrastructure; if [ "$1" = verify ]; then product; fi;;
rotation) . infra/local/scripts/verify.sh; . infra/local/scripts/rotation.sh; existing; rotation;;
test) sh infra/local/scripts/test.sh;;
+20
View File
@@ -0,0 +1,20 @@
# Operator input is JSON on stdin. Only credential values are stored in Vault.
configure_sendico() {
existing
cat > "$WORK/input"
jq -e '
(.organization_ref|test("^[0-9a-f]{24}$") and .!="000000000000000000000000") and
(.source_account_ref|test("^[0-9a-f]{24}$") and .!="000000000000000000000000") and
([.client_id,.device_id,.login,.password,.client_secret] | all(.[]; type=="string" and test("\\S"))) and
([.client_id,.device_id] | all(.[]; test("^[A-Za-z0-9._-]+$")))
' "$WORK/input" >/dev/null || fail 'Invalid Sendico operator input'
jq '{login,password,client_secret}' "$WORK/input" | put services/bff/sendico
mkdir -p "$STATE/sendico"
jq -r '"SENDICO_ORGANIZATION_REF="+.organization_ref+"\nSENDICO_SOURCE_ACCOUNT_REF="+.source_account_ref+"\nSENDICO_CLIENT_ID="+.client_id+"\nSENDICO_DEVICE_ID="+.device_id' "$WORK/input" | write "$STATE/sendico/settings.env"
{
cat infra/local/bff/runtime.yml
jq -r '"\n sendico:\n api_url: http://127.0.0.1:18081/api/v1\n organization_ref: "+.organization_ref+"\n source_account_ref: "+.source_account_ref+"\n token_file: /run/sendico/access-token"' "$WORK/input"
} | write "$STATE/sendico/runtime.yml" 644
provision_agents
echo 'Local Sendico connection configured. Run make up.'
}
+8
View File
@@ -4,4 +4,12 @@ umask 077
if [ "$SXCHNGE_SERVICE" = notifications ]; then
exec service -config.file=/etc/sxchnge/notifications.yml
fi
if [ "${SXCHNGE_SENDICO_LOCAL:-}" = 1 ]; then
attempt=0
until [ -s /run/sendico/access-token ] && [ "$(($(date +%s) - $(stat -c %Y /run/sendico/access-token)))" -lt 1500 ]; do
attempt=$((attempt+1))
[ "$attempt" -lt 120 ] || { echo 'Local Sendico session did not become ready' >&2; exit 1; }
sleep 2
done
fi
exec service -config.file=/etc/sxchnge/bff.yml
+21
View File
@@ -4,6 +4,7 @@ set -o pipefail
# Shell syntax and static analysis cover the actual container shell scripts.
shellcheck -s ash -x -S warning infra/local/scripts/dispatch.sh infra/local/scripts/test.sh
shellcheck -x -S warning infra/local/tools.sh infra/local/scripts/start-service.sh infra/local/mongo/start.sh infra/local/nats/start.sh infra/local/vault/start.sh
shellcheck -s ash -x -S warning infra/local/sendico/session.sh infra/local/sendico/check.sh
. infra/local/scripts/common.sh
. infra/local/scripts/images.sh
. infra/local/scripts/bootstrap.sh
@@ -93,3 +94,23 @@ retire_secrets
retire_secrets
jq -e '.=={"infrastructure/nats/auth":true}' "$WORK/retirement" >/dev/null
echo 'PASS: obsolete caller secrets retired idempotently; NATS authentication preserved'
# Local integration configuration keeps credentials out of runtime settings and
# rejects malformed operator inputs before mutating Vault or service identities.
. infra/local/scripts/sendico.sh
STATE="$WORK/sendico-state"; mkdir "$STATE"
existing() { :; }
put() { cat > "$WORK/sendico-credentials"; }
provision_agents() { touch "$WORK/sendico-agents"; }
printf '{}' > "$WORK/sendico-invalid"
if (configure_sendico < "$WORK/sendico-invalid") >/dev/null 2>&1; then fail 'Invalid Sendico configuration accepted'; fi
[ ! -e "$WORK/sendico-credentials" ] || fail 'Invalid Sendico input mutated credentials'
cat > "$WORK/sendico-input" <<'JSON'
{"organization_ref":"000000000000000000000001","source_account_ref":"000000000000000000000002","client_id":"fixture.client","device_id":"fixture-device","login":"fixture@example.test","password":"fixture-password","client_secret":"fixture-secret"}
JSON
configure_sendico < "$WORK/sendico-input" >/dev/null
jq -e 'keys==["client_secret","login","password"]' "$WORK/sendico-credentials" >/dev/null
[ -f "$WORK/sendico-agents" ] || fail 'Sendico identity was not provisioned'
if grep -Eq 'fixture-secret|fixture-password|fixture@example' "$STATE/sendico/settings.env" "$STATE/sendico/runtime.yml"; then fail 'Credential leaked into runtime configuration'; fi
grep -q 'source_account_ref: 000000000000000000000002' "$STATE/sendico/runtime.yml"
grep -q 'api_url: http://127.0.0.1:18081/api/v1' "$STATE/sendico/runtime.yml"
echo 'PASS: Sendico operator input validation, secret separation and local runtime configuration'
+14 -3
View File
@@ -2,11 +2,16 @@ policies() (
root_token=$VAULT_TOKEN
identity=''
trap 'if [ -n "$identity" ]; then printf "{}" | VAULT_TOKEN=$identity vault auth/token/revoke-self POST >/dev/null; fi' EXIT
for service in bff notifications nats; do
services='bff notifications nats'
if [ -f "$STATE/sendico/settings.env" ]; then services="$services sendico"; fi
for service in $services; do
directory="$STATE/agents/$service"
login=$(jq -n --rawfile role "$directory/role-id" --rawfile secret "$directory/secret-id" '{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' | vault auth/approle/login POST)
identity=$(printf '%s' "$login" | jq -er '.auth.client_token')
printf '%s' "$login" | jq '{token:.auth.client_token,paths:["kv/data/bootstrap/mongo","kv/data/infrastructure/mongo/bootstrap","sys/init","kv/data/bff","kv/data/caller","kv/data/services/notifications/callers/bff","kv/data/email","kv/data/nats","kv/data/tls"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Service can access bootstrap credentials'
if [ "$service" = sendico ]; then
printf '%s' "$login" | jq '{token:.auth.client_token,paths:["database/creds/bff","database/creds/notifications","kv/data/services/bff/jwt","kv/data/infrastructure/nats/auth"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Sendico session identity has unrelated secret access'
fi
VAULT_TOKEN=$(printf '%s' "$login" | jq -er '.auth.client_token')
printf '{}' | vault auth/token/revoke-self POST >/dev/null
VAULT_TOKEN=$root_token
@@ -44,12 +49,18 @@ infrastructure() {
# Resolve through Docker DNS while verifying the certificate for localhost.
https() { curl -sS --max-time 5 --cacert "$STATE/ca.pem" --connect-to localhost:5126:frontend:8443 "$@"; }
product() {
compose ps --format json | jq -se 'length==6 and all(.[];.Health=="healthy")' >/dev/null || fail 'Expected six healthy services'
expected=6
if [ -f "$STATE/sendico/settings.env" ]; then expected=7; fi
compose ps --format json | jq -se --argjson expected "$expected" 'length==$expected and all(.[];.Health=="healthy")' >/dev/null || fail "Expected $expected healthy services"
https -f https://localhost:5126/ >/dev/null
https -f https://localhost:5126/api/v1/health >/dev/null
echo 'PASS: six healthy services, frontend and proxied BFF use verified TLS; no email sent'
if [ "$expected" = 7 ]; then compose exec -T sendico sh /etc/sendico/check.sh; fi
echo "PASS: $expected healthy services, frontend and proxied BFF use verified TLS; no email sent"
}
layout() {
if [ -f "$STATE/sendico/settings.env" ]; then
kv services/bff/sendico | jq -e 'keys==["client_secret","login","password"]' >/dev/null || fail 'Sendico secret contains non-secret configuration'
fi
kv services/notifications/email/resend | jq -e 'keys==["api_key"]' >/dev/null || fail 'Non-secret email configuration remains in Vault'
for old in bootstrap/mongo bff caller services/notifications/callers/bff email nats tls frontend-tls notifications-tls; do
[ "$(vault "kv/metadata/$old" GET yes)" = null ] || fail "Obsolete KV entry remains: $old"