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
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:
@@ -431,9 +431,16 @@ For deploy-on-merge, set the repository variable `KEYCLOAK_URL` to the same valu
|
|||||||
With it set, the `localhost` port-forwards (§5) no longer log in: the issuer is one string.
|
With it set, the `localhost` port-forwards (§5) no longer log in: the issuer is one string.
|
||||||
|
|
||||||
Staff logins still hit the enforced OTP step. For a demo, set the repository variable
|
Staff logins still hit the enforced OTP step. For a demo, set the repository variable
|
||||||
`OTP_AUTOFILL=true` (chart value `demo.otpAutofill`): the `big-demo` login theme then
|
`OTP_AUTOFILL=true` (chart value `demo.otpAutofill`): Keycloak then uses the `big-demo`
|
||||||
fills in and submits the code from the fixture secret, so the step is visible but needs no
|
theme, which fills in and submits the code from the fixture secret, so the step is visible
|
||||||
authenticator. Keycloak restarts when the value flips. Demo only — the secret is committed.
|
but needs no authenticator. Keycloak restarts when the value flips. Demo only — the secret
|
||||||
|
is committed.
|
||||||
|
|
||||||
|
The theme lives in `infra/keycloak/themes/big-demo/` and is seeded as the `rr-kc-theme`
|
||||||
|
ConfigMap by `infra/helm/seed-configmaps.sh` on every deploy. Keycloak runs `start-dev`,
|
||||||
|
which doesn't cache themes, so an edit shows up about a minute after the ConfigMap changes.
|
||||||
|
A *new* theme file also needs a key in the seed script and a path in the keycloak `files`
|
||||||
|
in `values.yaml`.
|
||||||
|
|
||||||
One-time setup:
|
One-time setup:
|
||||||
|
|
||||||
|
|||||||
@@ -129,13 +129,17 @@ volumes:
|
|||||||
{{/*
|
{{/*
|
||||||
Env list from a map. Every value is run through `tpl`, so values.yaml can name
|
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
|
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" -}}
|
{{- define "big.env" -}}
|
||||||
{{- $root := index . 0 -}}
|
{{- $root := index . 0 -}}
|
||||||
{{- range $k, $v := index . 1 }}
|
{{- range $k, $v := index . 1 }}
|
||||||
|
{{- $val := tpl (toString $v) $root }}
|
||||||
|
{{- if $val }}
|
||||||
- name: {{ $k }}
|
- name: {{ $k }}
|
||||||
value: {{ tpl (toString $v) $root | quote }}
|
value: {{ $val | quote }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ spec:
|
|||||||
annotations:
|
annotations:
|
||||||
checksum/portal-config: {{ include "big.keycloakUrl" $ | sha256sum }}
|
checksum/portal-config: {{ include "big.keycloakUrl" $ | sha256sum }}
|
||||||
{{- end }}
|
{{- 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 }}
|
{{- end }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 8 }}
|
{{- 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 }}
|
|
||||||
@@ -32,8 +32,8 @@ keycloakUrl: ""
|
|||||||
|
|
||||||
demo:
|
demo:
|
||||||
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
|
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
|
||||||
# demo shows MFA enforced without an authenticator (templates/keycloak-theme.yaml).
|
# demo shows MFA enforced without an authenticator: makes the big-demo theme
|
||||||
# Demo only: the secret is committed.
|
# (infra/keycloak/themes/big-demo) Keycloak's default. Demo only: the secret is committed.
|
||||||
otpAutofill: false
|
otpAutofill: false
|
||||||
|
|
||||||
# Set when pulling from a private registry (e.g. the Gitea Container Registry).
|
# Set when pulling from a private registry (e.g. the Gitea Container Registry).
|
||||||
@@ -281,15 +281,17 @@ workloads:
|
|||||||
# this issuer back, which is what browser tokens carry (infra/host-browser.yml).
|
# this issuer back, which is what browser tokens carry (infra/host-browser.yml).
|
||||||
KC_HOSTNAME: '{{ include "big.keycloakUrl" . }}'
|
KC_HOSTNAME: '{{ include "big.keycloakUrl" . }}'
|
||||||
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
|
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
|
||||||
# keycloak.v2 plus the demo.otpAutofill script (templates/keycloak-theme.yaml).
|
# Only rendered with demo.otpAutofill (big.env skips empty values); off, Keycloak
|
||||||
KC_SPI_THEME_DEFAULT: big-demo
|
# keeps its stock theme and the mounted big-demo theme is unused.
|
||||||
|
KC_SPI_THEME_DEFAULT: '{{ if .Values.demo.otpAutofill }}big-demo{{ end }}'
|
||||||
ports: [{ name: http, port: 8080 }]
|
ports: [{ name: http, port: 8080 }]
|
||||||
# TCP, not /health/ready on the management port: nothing here gates on realm
|
# TCP, not /health/ready on the management port: nothing here gates on realm
|
||||||
# import, and a wrong health path would leave the Service with no endpoints.
|
# import, and a wrong health path would leave the Service with no endpoints.
|
||||||
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 15 }
|
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 15 }
|
||||||
files:
|
files:
|
||||||
- { configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }
|
- { configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }
|
||||||
- configMap: kc-theme
|
# infra/keycloak/themes/big-demo, seeded by infra/helm/seed-configmaps.sh.
|
||||||
|
- configMap: rr-kc-theme
|
||||||
mountPath: /opt/keycloak/themes/big-demo
|
mountPath: /opt/keycloak/themes/big-demo
|
||||||
items:
|
items:
|
||||||
- { key: login.properties, path: login/theme.properties }
|
- { key: login.properties, path: login/theme.properties }
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ seed() { # name <kubectl --from-file args...>
|
|||||||
seed rr-oz-config --from-file="$repo/infra/openzaak/setup_configuration/"
|
seed rr-oz-config --from-file="$repo/infra/openzaak/setup_configuration/"
|
||||||
seed rr-nrc-config --from-file="$repo/infra/opennotificaties/setup_configuration/"
|
seed rr-nrc-config --from-file="$repo/infra/opennotificaties/setup_configuration/"
|
||||||
seed rr-kc-realms --from-file="$repo/infra/keycloak/realms/"
|
seed rr-kc-realms --from-file="$repo/infra/keycloak/realms/"
|
||||||
|
# The big-demo login theme (demo.otpAutofill). ConfigMap keys are flat, so each
|
||||||
|
# file gets a key here and its path back in the keycloak `files` in values.yaml.
|
||||||
|
theme="$repo/infra/keycloak/themes/big-demo"
|
||||||
|
seed rr-kc-theme \
|
||||||
|
--from-file=login.properties="$theme/login/theme.properties" \
|
||||||
|
--from-file=otp-autofill.js="$theme/login/resources/js/otp-autofill.js" \
|
||||||
|
--from-file=account.properties="$theme/account/theme.properties" \
|
||||||
|
--from-file=admin.properties="$theme/admin/theme.properties" \
|
||||||
|
--from-file=email.properties="$theme/email/theme.properties"
|
||||||
seed rr-objecttypen-config --from-file="$repo/infra/objecttypen/setup_configuration/"
|
seed rr-objecttypen-config --from-file="$repo/infra/objecttypen/setup_configuration/"
|
||||||
seed rr-objecten-config --from-file="$repo/infra/objecten/setup_configuration/"
|
seed rr-objecten-config --from-file="$repo/infra/objecten/setup_configuration/"
|
||||||
# register.py + the RegisterRecord JSON schema (the __pycache__ dir is skipped:
|
# register.py + the RegisterRecord JSON schema (the __pycache__ dir is skipped:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# The chart makes big-demo the default for every theme type, and Keycloak does not
|
||||||
|
# fall back for a type a theme lacks (the account page then fails), so each type is
|
||||||
|
# declared as a plain child of Keycloak 26's own default.
|
||||||
|
parent=keycloak.v3
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# The chart makes big-demo the default for every theme type, and Keycloak does not
|
||||||
|
# fall back for a type a theme lacks (the admin page then fails), so each type is
|
||||||
|
# declared as a plain child of Keycloak 26's own default.
|
||||||
|
parent=keycloak.v2
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# The chart makes big-demo the default for every theme type, and Keycloak does not
|
||||||
|
# fall back for a type a theme lacks (the email page then fails), so each type is
|
||||||
|
# declared as a plain child of Keycloak 26's own default.
|
||||||
|
parent=keycloak
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 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();
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Demo login theme for the public Talos deployment: keycloak.v2 plus a script that
|
||||||
|
# fills in and submits the medewerker OTP step from the committed fixture secret
|
||||||
|
# (docs/runbooks/keycloak.md). Only used when the chart's demo.otpAutofill is on —
|
||||||
|
# it then becomes Keycloak's default theme. Never enable it anywhere real.
|
||||||
|
#
|
||||||
|
# Add styles, messages or template overrides here as in any Keycloak theme
|
||||||
|
# (https://www.keycloak.org/ui-customization/themes); new files must also be
|
||||||
|
# listed in infra/helm/seed-configmaps.sh and the keycloak `files` in values.yaml.
|
||||||
|
parent=keycloak.v2
|
||||||
|
import=common/keycloak
|
||||||
|
scripts=js/otp-autofill.js
|
||||||
Reference in New Issue
Block a user