Connect local SXCHNGE to the Sendico stand
This commit was merged in pull request #53.
This commit is contained in:
1 parent
34d8ef18d1
commit
a5689876ff
14 files changed
+336
-9
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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")"
|
||||
|
||||
@@ -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;;
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 077
|
||||
check=$(mktemp -d /private/check.XXXXXX)
|
||||
trap 'rm -rf "$check"' EXIT
|
||||
printf 'Authorization: Bearer %s\n' "$(cat /run/sendico/access-token)" > "$check/header"
|
||||
for path in "payments/fx/currencies/$SENDICO_ORGANIZATION_REF" "ledger_accounts/$SENDICO_ORGANIZATION_REF"; do
|
||||
curl -fsS --max-time 15 -H "@$check/header" "http://127.0.0.1:18081/api/v1/$path" > "$check/response" 2>/dev/null || { echo 'Local Sendico authenticated API check failed' >&2; exit 1; }
|
||||
jq -e '.status=="success"' "$check/response" >/dev/null
|
||||
done
|
||||
echo 'PASS: local Sendico authenticated currency and ledger APIs are accessible'
|
||||
@@ -0,0 +1,30 @@
|
||||
# Enabled by make sendico; settings contain no credentials.
|
||||
services:
|
||||
bff:
|
||||
environment:
|
||||
SXCHNGE_SENDICO_LOCAL: '1'
|
||||
volumes:
|
||||
- ./.local/dev/sendico/runtime.yml:/etc/sxchnge/bff.yml:ro
|
||||
- sendico-session:/run/sendico:ro
|
||||
|
||||
sendico:
|
||||
image: sxchnge-local/tools:dev
|
||||
restart: unless-stopped
|
||||
# Keep the adapter's loopback-only HTTP rule; forward to the public stand API.
|
||||
network_mode: service:bff
|
||||
entrypoint: [/bin/sh, /etc/sendico/session.sh]
|
||||
env_file: ./.local/dev/sendico/settings.env
|
||||
volumes:
|
||||
- ./infra/local/sendico/session.sh:/etc/sendico/session.sh:ro
|
||||
- ./infra/local/sendico/check.sh:/etc/sendico/check.sh:ro
|
||||
- ./.local/dev/agents/sendico:/etc/agent:ro
|
||||
- sendico-session:/run/sendico
|
||||
tmpfs: [/private:mode=0700]
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, 'test -f /private/ready && test $$(($$(date +%s) - $$(stat -c %Y /private/ready))) -lt 1500 && curl -fsS --max-time 2 http://127.0.0.1:18081/api/v1/health >/dev/null']
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
|
||||
volumes:
|
||||
sendico-session:
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 077
|
||||
mkdir -p /private /run/sendico
|
||||
chmod 755 /run/sendico
|
||||
cd /private
|
||||
socat TCP4-LISTEN:18081,bind=127.0.0.1,reuseaddr,fork TCP4:host.docker.internal:8080 &
|
||||
bridge=$!
|
||||
trap 'kill "$bridge" 2>/dev/null || true' EXIT
|
||||
trap 'exit 0' INT TERM
|
||||
|
||||
session() {
|
||||
jq -n --rawfile role /etc/agent/role-id --rawfile secret /etc/agent/secret-id \
|
||||
'{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' > login.json
|
||||
curl -fsS --max-time 15 -H 'Content-Type: application/json' --data-binary @login.json \
|
||||
http://vault:8200/v1/auth/approle/login > vault.json 2>/dev/null || return 1
|
||||
jq -er '"X-Vault-Token: "+.auth.client_token' vault.json > vault.header || return 1
|
||||
curl -fsS --max-time 15 -H @vault.header http://vault:8200/v1/kv/data/services/bff/sendico > credentials.json 2>/dev/null || return 1
|
||||
jq -e --arg client "$SENDICO_CLIENT_ID" --arg device "$SENDICO_DEVICE_ID" \
|
||||
'.data.data | {login:{login,password},clientSecret:.client_secret,clientId:$client,deviceId:$device}' credentials.json > request.json || return 1
|
||||
curl -fsS --max-time 15 -H @vault.header -X POST http://vault:8200/v1/auth/token/revoke-self >/dev/null 2>&1 || return 1
|
||||
rm -f vault.header vault.json credentials.json login.json
|
||||
curl -fsS --max-time 20 -H 'Content-Type: application/json' --data-binary @request.json \
|
||||
http://127.0.0.1:18081/api/v1/accounts/login/api > response.json 2>/dev/null || return 1
|
||||
rm -f request.json
|
||||
jq -er '.data.accessToken.token' response.json > token 2>/dev/null || return 1
|
||||
jq -e --arg source "$SENDICO_SOURCE_ACCOUNT_REF" \
|
||||
'.data.accessToken.token | split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | .id==$source and .pending!=true and .exp>(now+1500)' response.json >/dev/null 2>&1 || return 1
|
||||
cp token /run/sendico/access-token.new
|
||||
chmod 444 /run/sendico/access-token.new
|
||||
mv /run/sendico/access-token.new /run/sendico/access-token
|
||||
rm -f token response.json
|
||||
touch ready
|
||||
}
|
||||
|
||||
while :; do
|
||||
if session; then
|
||||
echo 'Local Sendico session ready; next renewal in 20 minutes.'
|
||||
sleep 1200 & wait $!
|
||||
else
|
||||
echo 'Local Sendico session unavailable; check stand health and integration credentials. Retrying in 5 minutes.' >&2
|
||||
rm -f request.json response.json token vault.header vault.json credentials.json login.json
|
||||
sleep 300 & wait $!
|
||||
fi
|
||||
done
|
||||
Reference in new issue
Block a user