key-cape/scripts/test-scenario-b.sh
tegwick 3b72834bca
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 43s
Reconcile packaging, bootstrap and migration credential handling
Closes gap G09: five loosely related defects.

lldap-export took the service account password on argv, where ps exposes it to
any local user and shell history and process accounting capture it. It now
prefers KEYCAPE_LLDAP_BIND_PW or --bind-pw-file; --bind-pw still works but warns,
deprecated rather than removed because existing runbooks use it and breaking them
silently would be worse than one more cycle of exposure. Conflicting sources are
rejected instead of silently ranked, since an operator otherwise cannot tell
which bind was attempted. Both migration scripts pass the password by
environment now.

The canonical export, generated LDIF and Keycloak realm were written 0644. None
carries credential material, but the snapshot is every username, display name,
email and group membership in the estate, and it tends to land in /tmp. All three
are 0600.

The image packaged keycape alone, so the validator and migration binaries needed
a Go toolchain on the host -- which defeats shipping an image for the cutover
work they exist to support. All five ship; the issuer stays the entrypoint.
Verified by building the image and running each binary inside it.

The publish workflow named 92.205.130.254:32166 while the cluster runs
forgejo.coulomb.social/coulomb/key-cape. It now defaults to the recorded name and
stays overridable by a repository variable. This repository cannot verify that
the runner resolves that hostname or that the registry credentials are valid for
it; if the next publish fails, set the REGISTRY variable back to the address.

docker-compose.dev.yml mounts a private key and Authelia material that are
correctly absent from the checkout. scripts/bootstrap-dev.sh generates them
locally under a restrictive umask rather than chmodding afterwards, so the key is
never briefly world-readable. Everything it writes is git-ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-08 09:51:59 +02:00

70 lines
2.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# test-scenario-b.sh — Scenario B: IAM swap (KeyCape -> Keycloak, same LLDAP directory).
#
# Exports the canonical directory, transforms it into a Keycloak realm, starts
# Keycloak over that realm, and runs the external conformance suite against it.
#
# Prerequisites: docker, docker compose, and `make -C src build` (root `make build` only compiles; the src target installs into bin/).
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
BUILD_DIR="$REPO_ROOT/build/scenario-b"
COMPOSE="docker compose -f docker-compose.scenario-b.yml"
ISSUER_URL="http://localhost:8080/realms/netkingdom"
die() { echo "scenario-b: $*" >&2; exit 1; }
# Check every prerequisite up front: failing half way through leaves a running
# stack and a half-written realm, which is harder to diagnose than not starting.
for binary in lldap-export keycape-to-keycloak; do
[ -x "bin/$binary" ] || die "bin/$binary is missing; run 'make -C src build' first"
done
command -v docker >/dev/null || die "docker is not installed"
command -v go >/dev/null || die "go is not installed"
cleanup() { $COMPOSE down -v >/dev/null 2>&1 || true; }
trap cleanup EXIT
mkdir -p "$BUILD_DIR/realm"
echo "=== Scenario B: IAM replacement ==="
echo "--- Step 1: start the directory ---"
$COMPOSE up -d lldap
timeout 120 bash -c 'until (exec 3<>/dev/tcp/127.0.0.1/3890) 2>/dev/null; do sleep 2; done' \
|| die "LLDAP did not accept connections within 120s"
echo "--- Step 2: export the canonical directory ---"
# The password goes through the environment: argv is visible to any local
# user via ps (KEY-WP-0026).
KEYCAPE_LLDAP_BIND_PW="${LLDAP_BIND_PW:-adminpassword}" \
./bin/lldap-export \
--url "${LLDAP_URL:-ldap://localhost:3890}" \
--bind-dn "${LLDAP_BIND_DN:-cn=admin,ou=people,dc=netkingdom,dc=local}" \
--base-dn "${LLDAP_BASE_DN:-dc=netkingdom,dc=local}" \
--output "$BUILD_DIR/canonical-export.yaml"
echo "--- Step 3: transform into a Keycloak realm ---"
./bin/keycape-to-keycloak \
--input "$BUILD_DIR/canonical-export.yaml" \
--realm netkingdom \
--issuer "${ISSUER:-$ISSUER_URL}" \
--output "$BUILD_DIR/realm/netkingdom-realm.json"
echo "--- Step 4: start Keycloak over the migrated realm ---"
$COMPOSE up -d keycloak
timeout 180 bash -c "until curl -sf '$ISSUER_URL/.well-known/openid-configuration' >/dev/null; do sleep 3; done" \
|| die "Keycloak did not serve discovery within 180s"
echo "--- Step 5: run the external conformance suite against Keycloak ---"
# The suite reads KEYCAPE_TEST_ISSUER and talks to the running issuer over HTTP.
# Run it from the module directory: the Go module root is src/, not the repo root.
KEYCAPE_TEST_ISSUER="$ISSUER_URL" \
go -C src test ./tests/conformance/... -v -count=1
echo "=== Scenario B PASSED ==="
echo "Note: this proves the migrated realm serves a conforming OIDC surface."
echo "It does not prove credential, MFA or relying-party behaviour was preserved."