refactor(k8s): keep the big-demo theme as real files under infra/keycloak/themes (refs #177)
CI / lint (pull_request) Successful in 1m55s
CI / k8s (pull_request) Successful in 9s
CI / build (pull_request) Successful in 1m27s
CI / unit (pull_request) Successful in 1m51s
CI / frontend (pull_request) Successful in 3m24s
CI / mutation (pull_request) Successful in 6m40s
CI / verify-stack (pull_request) Failing after 1m1s

The theme was inline text in a Helm template. It now lives next to the realms
and is seeded as rr-kc-theme by seed-configmaps.sh, like every other file
input, so it can be edited as a normal Keycloak theme. demo.otpAutofill now
only decides whether KC_SPI_THEME_DEFAULT is set (big.env skips values that
render empty); off, Keycloak keeps its stock theme.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
not
2026-09-25 12:15:35 +02:00
co-authored by Claude Opus 5.5
parent 6cd2268be9
commit 0ea562db52
11 changed files with 79 additions and 77 deletions
@@ -129,13 +129,17 @@ volumes:
{{/*
Env list from a map. Every value is run through `tpl`, so values.yaml can name
cluster-internal hosts ({{ .Release.Namespace }}) and the node address
({{ .Values.host }}) without the chart hard-coding either.
({{ .Values.host }}) without the chart hard-coding either. A value that renders
empty is left out, which is how a setting is made conditional on a chart value.
*/}}
{{- define "big.env" -}}
{{- $root := index . 0 -}}
{{- range $k, $v := index . 1 }}
{{- $val := tpl (toString $v) $root }}
{{- if $val }}
- name: {{ $k }}
value: {{ tpl (toString $v) $root | quote }}
value: {{ $val | quote }}
{{- end }}
{{- end }}
{{- end -}}
@@ -30,11 +30,6 @@ spec:
annotations:
checksum/portal-config: {{ include "big.keycloakUrl" $ | sha256sum }}
{{- end }}
{{- /* Keycloak reads the theme at startup, so it restarts when the toggle flips. */}}
{{- if eq .configMap "kc-theme" }}
annotations:
checksum/otp-autofill: {{ $.Values.demo.otpAutofill | toString | sha256sum }}
{{- end }}
{{- end }}
labels:
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 8 }}
@@ -1,62 +0,0 @@
{{- /*
Keycloak theme `big-demo`: keycloak.v2 plus one login script. It is always
mounted and always the default theme (KC_SPI_THEME_DEFAULT), so the only thing
`demo.otpAutofill` switches is what that script does. Off, it is empty and the
login is exactly keycloak.v2.
KC_SPI_THEME_DEFAULT covers every theme type, and Keycloak does not fall back
for a type the theme lacks (the account console 500s), so account, admin and
email are declared too, each a plain child of Keycloak 26's own default.
On, the medewerker OTP step computes the code from the realm fixture secret
(docs/runbooks/keycloak.md) and submits it: the demo still shows MFA being
enforced without anyone needing an authenticator. The secret is committed and
shared, so this is a demo convenience only — never enable it anywhere real.
*/ -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: kc-theme
labels:
{{- include "big.labels" (dict "root" $ "name" "kc-theme") | nindent 4 }}
data:
login.properties: |
parent=keycloak.v2
import=common/keycloak
scripts=js/otp-autofill.js
account.properties: |
parent=keycloak.v3
admin.properties: |
parent=keycloak.v2
email.properties: |
parent=keycloak
otp-autofill.js: |
{{- if .Values.demo.otpAutofill }}
// RFC 6238 with Keycloak's default policy (HmacSHA1, 6 digits, 30 s) over the
// raw bytes of the medewerker fixture secret — same as tests/e2e/keycloak-login.ts.
document.addEventListener('DOMContentLoaded', async () => {
const input = document.querySelector('input[name="otp"]');
if (!input || !input.form) return;
const key = await crypto.subtle.importKey('raw',
new TextEncoder().encode('BIGMEDEWERKEROTPSEED'), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
// A code is single-use, so a second login in the same window spends the next
// counter (Keycloak's look-ahead accepts it). Past that, fill but don't submit,
// so a rejected code can't turn into a submit loop.
const now = Math.floor(Date.now() / 30000);
let last = -1;
try { last = Number(sessionStorage.getItem('big-otp-counter')) || -1; } catch {}
const counter = Math.max(now, last + 1);
const msg = new DataView(new ArrayBuffer(8));
msg.setBigUint64(0, BigInt(counter));
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg.buffer));
const o = mac[19] & 0x0f;
const n = ((mac[o] & 0x7f) << 24 | mac[o + 1] << 16 | mac[o + 2] << 8 | mac[o + 3]) % 1e6;
input.value = String(n).padStart(6, '0');
if (counter > now + 1) return;
try { sessionStorage.setItem('big-otp-counter', String(counter)); } catch {}
input.form.requestSubmit();
});
{{- else }}
// demo.otpAutofill is off: the OTP step is entered by hand.
{{- end }}