#!/usr/bin/env bash
# Real demos for P110 "SAML vs OAuth vs OpenID Connect: What's the Difference?". Made on the Mac with Docker Desktop.
# Five containers on their own network, with names from the reserved .test domain (RFC 6761):
#   maple     id.maple.test, "Maple ID": the sign-in service, playing the part of a Google or Microsoft account
#             (Keycloak, one realm, one user, a SAML app and an OAuth / OpenID Connect app)
#   calendar  calendar.maple.test: Maple's calendar API (lab/calendar_api.py), which accepts access tokens
#   payroll   payroll.test: the clinic's payroll site, sign-in through SAML 2.0 (lab/payroll.py)
#   remindly  remindly.test: a reminder app; OAuth 2.0 to read the calendar, OpenID Connect to sign in
#   laptop    Ana's laptop at the dental clinic: a real Chromium (Playwright's build) driven by lab/browser
# Defensive only (owner rule): nothing here steals, captures or relays anything; the refusals shown are services
# protecting themselves. The lab's certificate authority is made fresh on every run.
# Every command shown is typed into a real interactive bash in a pty (demo_driver.py).
set -euo pipefail
cd "$(dirname "$0")"
P=ce-p110
SECRET=lab-only-remindly-secret     # Remindly's client secret, registered in lab/maple-realm.json (lab only)
ADMIN_PW=lab-only-admin
SITES="id.maple.test calendar.maple.test payroll.test remindly.test"
cleanup() { docker rm -f $P-maple $P-calendar $P-payroll $P-remindly $P-laptop >/dev/null 2>&1 || true; docker network rm $P-net >/dev/null 2>&1 || true; }
cleanup
[ -n "${KEEP:-}" ] || trap cleanup EXIT
T=$(mktemp -d)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 2 -subj "/CN=Lab CA" \
  -keyout "$T/ca.key" -out "$T/ca.crt" 2>/dev/null
for s in $SITES; do
  mkdir -p "$T/$s"
  openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=$s" \
    -keyout "$T/$s/site.key" -out "$T/$s/site.csr" 2>/dev/null
  printf "subjectAltName=DNS:%s\n" "$s" > "$T/$s/ext"
  openssl x509 -req -in "$T/$s/site.csr" -CA "$T/ca.crt" -CAkey "$T/ca.key" -CAcreateserial -days 2 \
    -extfile "$T/$s/ext" -out "$T/$s/site.crt" 2>/dev/null
  chmod 644 "$T/$s/site.key"     # read by the service's own user inside the container
done
docker build -q -t $P-maple -f lab/Dockerfile.maple lab >/dev/null
docker build -q -t $P-app -f lab/Dockerfile.app lab >/dev/null
docker build -q -t $P-laptop -f lab/Dockerfile.laptop lab >/dev/null
docker network create $P-net >/dev/null
docker create --name $P-maple --hostname maple --network $P-net --network-alias id.maple.test \
  --sysctl net.ipv4.ip_unprivileged_port_start=0 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=$ADMIN_PW \
  $P-maple start-dev --import-realm \
  --https-port=443 --http-enabled=false --hostname=https://id.maple.test \
  --https-certificate-file=/opt/keycloak/conf/tls/site.crt \
  --https-certificate-key-file=/opt/keycloak/conf/tls/site.key >/dev/null
docker cp "$T/id.maple.test" $P-maple:/opt/keycloak/conf/tls
docker cp "$T/ca.crt" $P-maple:/opt/keycloak/conf/lab-ca.crt
docker start $P-maple >/dev/null
for app in calendar:calendar.maple.test:calendar_api.py payroll:payroll.test:payroll.py remindly:remindly.test:remindly.py; do
  IFS=: read -r name site prog <<< "$app"
  docker create --name $P-$name --hostname $name --network $P-net --network-alias $site \
    -e CLIENT_SECRET=$SECRET $P-app python3 -u /srv/$prog >/dev/null
  docker cp "$T/$site" $P-$name:/srv/tls
  docker cp "$T/ca.crt" $P-$name:/usr/local/share/ca-certificates/lab-ca.crt
done
docker run -d --name $P-laptop --hostname laptop --network $P-net --ipc=host $P-laptop sleep infinity >/dev/null
docker cp "$T/ca.crt" $P-laptop:/tmp/lab-ca.crt
docker cp ../../browser_shots.py $P-laptop:/usr/local/lib/python3.12/dist-packages/browser_shots.py
rm -rf "$T"
docker exec -u ana $P-laptop sh -c 'mkdir -p ~/.pki/nssdb ~/shots && certutil -d sql:$HOME/.pki/nssdb -N --empty-password \
  && certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "Lab CA" -i /tmp/lab-ca.crt \
  && printf "mint-floss-42\n" > ~/.maple-password && chmod 600 ~/.maple-password \
  && echo "export SHOTS=/home/ana/shots" >> ~/.bashrc'
docker exec $P-laptop sh -c 'cp /tmp/lab-ca.crt /usr/local/share/ca-certificates/lab-ca.crt && update-ca-certificates >/dev/null 2>&1'
for i in $(seq 150); do
  docker exec $P-laptop curl -sf https://id.maple.test/realms/maple/.well-known/openid-configuration >/dev/null 2>&1 && break
  sleep 2
done
# Off screen: the two calendar permissions (scopes) Maple ID can hand out, each addressed to the calendar API, and
# Remindly may ask for them. Done with Keycloak's own admin tool.
KC="docker exec $P-maple /opt/keycloak/bin/kcadm.sh"
docker exec $P-maple sh -c 'keytool -importcert -noprompt -alias lab -file /opt/keycloak/conf/lab-ca.crt \
  -keystore /tmp/trust.jks -storepass changeit >/dev/null 2>&1'
$KC config truststore --trustpass changeit /tmp/trust.jks
$KC config credentials --server https://id.maple.test --realm master --user admin --password $ADMIN_PW >/dev/null 2>&1
CID=$($KC get clients -r maple -q clientId=remindly --fields id --format csv --noquotes)
for sc in "calendar.read:View your calendar" "calendar.write:Add and change appointments"; do
  IFS=: read -r scope words <<< "$sc"
  SID=$(docker exec -i $P-maple /opt/keycloak/bin/kcadm.sh create client-scopes -r maple -i -f - <<JSON
{"name": "$scope", "protocol": "openid-connect",
 "attributes": {"display.on.consent.screen": "true", "consent.screen.text": "$words", "include.in.token.scope": "true"},
 "protocolMappers": [{"name": "calendar audience", "protocol": "openid-connect", "protocolMapper": "oidc-audience-mapper",
   "config": {"included.custom.audience": "https://calendar.maple.test", "access.token.claim": "true", "id.token.claim": "false"}}]}
JSON
)
  $KC update clients/$CID/optional-client-scopes/$SID -r maple
done
for name in calendar payroll remindly; do
  docker start $P-$name >/dev/null
  docker exec $P-$name sh -c 'update-ca-certificates >/dev/null 2>&1'
done
for name in calendar payroll remindly; do
  for i in $(seq 30); do docker logs $P-$name 2>&1 | grep -q listening && break; sleep 1; done
done
[ -n "${SETUP_ONLY:-}" ] && { echo "lab up"; trap - EXIT; exit 0; }
python3 - "$P" < demo_driver.py
