From eaab062491305b5a96c098ab4638055ed1b7e4e6 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Thu, 13 Mar 2025 15:17:36 +0000 Subject: [PATCH 01/62] Add OpenID IngressRoute --- deploy/templates/openid/openid-ingress.yaml | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 deploy/templates/openid/openid-ingress.yaml diff --git a/deploy/templates/openid/openid-ingress.yaml b/deploy/templates/openid/openid-ingress.yaml new file mode 100644 index 000000000..900e648a2 --- /dev/null +++ b/deploy/templates/openid/openid-ingress.yaml @@ -0,0 +1,23 @@ +{{ if .Values.openid.enabled }} +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: openid-ingressroute + namespace: {{ .Release.Namespace }} +spec: + entryPoints: + - {{ .Values.acs.secure | ternary "websecure" "web" }} + routes: + - match: Host(`openid.{{.Values.acs.baseUrl | required "values.acs.baseUrl is required"}}`) + kind: Rule + services: + - name: openid + port: 80 + namespace: {{ .Release.Namespace }} + {{- if .Values.acs.secure }} + tls: + secretName: {{ coalesce .Values.openid.tlsSecretName .Values.acs.tlsSecretName }} + domains: + - main: openid.{{.Values.acs.baseUrl | required "values.acs.baseUrl is required"}} + {{- end -}} +{{- end -}} From fb59dd854e237994b4f21583db591d72920f1bac Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Thu, 13 Mar 2025 16:34:14 +0000 Subject: [PATCH 02/62] Add Keycloak deployment --- .../templates/openid/openid-admin-user.yaml | 17 +++++ deploy/templates/openid/openid.yaml | 64 +++++++++++++++++++ deploy/values.yaml | 10 ++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 deploy/templates/openid/openid-admin-user.yaml create mode 100644 deploy/templates/openid/openid.yaml diff --git a/deploy/templates/openid/openid-admin-user.yaml b/deploy/templates/openid/openid-admin-user.yaml new file mode 100644 index 000000000..6f318aa32 --- /dev/null +++ b/deploy/templates/openid/openid-admin-user.yaml @@ -0,0 +1,17 @@ +{{- if .Values.openid.enabled }} +{{- if not (lookup "v1" "Secret" .Release.Namespace "openid-admin-user") }} + +apiVersion: v1 +kind: Secret +metadata: + name: "openid-admin-user" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/resource-policy": "keep" +type: Opaque +data: + username: {{ (printf "admin@%s" (.Values.identity.realm | required "values.identity.realm is required!") | b64enc) | quote }} + password: {{ (printf "" | b64enc) | quote }} + +{{- end }} +{{- end -}} diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml new file mode 100644 index 000000000..0014eca2e --- /dev/null +++ b/deploy/templates/openid/openid.yaml @@ -0,0 +1,64 @@ +{{ if .Values.openid.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openid + namespace: {{ .Release.Namespace }} + labels: + component: openid +spec: + replicas: {{ .Values.openid.replicas | default 1 }} + selector: + matchLabels: + component: openid + template: + metadata: + labels: + component: openid + factory-plus.service: openid + spec: + {{- with .Values.acs.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: openid + image: "{{ .Values.openid.image.repository }}:{{ .Values.openid.image.tag }}" + imagePullPolicy: {{ .Values.openid.image.pullPolicy }} + args: ["start-dev"] + env: + - name: KEYCLOAK_ADMIN + valueFrom: + secretKeyRef: + name: openid-admin-user + key: username + - name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: openid-admin-user + key: password + - name: KC_PROXY + value: "edge" + - name: KC_HEALTH_ENABLED + value: "true" + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /health/ready + port: 9000 +--- +apiVersion: v1 +kind: Service +metadata: + name: openid + namespace: {{ .Release.Namespace }} +spec: + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + factory-plus.service: openid +{{- end }} diff --git a/deploy/values.yaml b/deploy/values.yaml index b7309f5fa..099962bbb 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -241,7 +241,7 @@ shell: repository: acs-krb-utils # XXX This should probably be included in acs-krb-utils -curl: +curl: image: registry: docker.io repository: appropriate/curl @@ -395,5 +395,13 @@ influxdb2: pdb: create: false +openid: + enabled: true + replicas: 1 + image: + repository: quay.io/keycloak/keycloak + tag: 26.1.1 + pullPolicy: IfNotPresent + cert-manager: fullnameOverride: "cert-manager" From 45aac271693c1541b9a0825cd313bbbadd12f77d Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Mon, 17 Mar 2025 00:26:31 +0000 Subject: [PATCH 03/62] Add startup realm importing --- deploy/templates/openid/openid.yaml | 25 +- deploy/templates/openid/realm-config.yaml | 2740 +++++++++++++++++++++ deploy/templates/openid/secrets.yaml | 14 + 3 files changed, 2778 insertions(+), 1 deletion(-) create mode 100644 deploy/templates/openid/realm-config.yaml create mode 100644 deploy/templates/openid/secrets.yaml diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index 0014eca2e..dde945f94 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -25,7 +25,7 @@ spec: - name: openid image: "{{ .Values.openid.image.repository }}:{{ .Values.openid.image.tag }}" imagePullPolicy: {{ .Values.openid.image.pullPolicy }} - args: ["start-dev"] + args: ["start-dev", "--import-realm"] env: - name: KEYCLOAK_ADMIN valueFrom: @@ -41,6 +41,8 @@ spec: value: "edge" - name: KC_HEALTH_ENABLED value: "true" + - name: KRB5_CONFIG + value: /config/krb5-conf/krb5.conf ports: - name: http containerPort: 8080 @@ -48,6 +50,27 @@ spec: httpGet: path: /health/ready port: 9000 + volumeMounts: + - name: realm-config + mountPath: /opt/keycloak/data/import + readOnly: true + - name: krb5-conf + mountPath: /config/krb5-conf + volumes: + - name: realm-config + configMap: + name: keycloak-realm-config + - name: krb5-conf + configMap: + name: krb5-conf + - name: krb5-keytabs + secret: + secretName: krb5-keytabs + items: + - path: client + key: sv1openid + - path: server + key: http.openid --- apiVersion: v1 kind: Service diff --git a/deploy/templates/openid/realm-config.yaml b/deploy/templates/openid/realm-config.yaml new file mode 100644 index 000000000..a5a72e755 --- /dev/null +++ b/deploy/templates/openid/realm-config.yaml @@ -0,0 +1,2740 @@ +{{- if .Values.openid.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-realm-config + namespace: {{ .Release.Namespace }} +data: + realm-config.json: | + { + "id": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", + "realm": "factory_plus", + "displayName": "Factory+", + "displayNameHtml": "
Factory+
", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 60, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "bruteForceStrategy": "MULTIPLE", + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "6e9a20eb-25fe-4c56-acd7-89dcf59246ac", + "name": "admin", + "description": "${role_admin}", + "composite": true, + "composites": { + "realm": [ + "create-realm" + ], + "client": { + "master-realm": [ + "view-realm", + "manage-realm", + "create-client", + "view-events", + "view-users", + "manage-identity-providers", + "view-clients", + "query-realms", + "manage-users", + "manage-clients", + "view-authorization", + "manage-authorization", + "manage-events", + "query-clients", + "impersonation", + "view-identity-providers", + "query-groups", + "query-users" + ] + } + }, + "clientRole": false, + "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", + "attributes": {} + }, + { + "id": "c47f84ec-e052-46a9-88bb-007b69f97500", + "name": "create-realm", + "description": "${role_create-realm}", + "composite": false, + "clientRole": false, + "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", + "attributes": {} + }, + { + "id": "02f9d24c-49f9-448d-b713-097cd865c156", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", + "attributes": {} + }, + { + "id": "5ee103d6-29d7-4223-a9c5-602c7252bf95", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", + "attributes": {} + }, + { + "id": "d44417f0-73b3-46a3-b518-4aa287ac2a11", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "003a91f9-6480-42d1-a059-11fc8042889c", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "8e09177a-8893-44a6-ac06-9c47e8c18318", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "0acc8e69-2ed7-4928-9162-5e818910292a", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "811e9a81-5d39-492e-8ff0-59742b5060c8", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "a02eacb5-ac1d-461a-8d95-6982985dfb90", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "91440da6-0d24-41c5-852f-b3efa66a6656", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "896539b3-4f55-4ff4-8fa0-8c87c022ed75", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "87b45e17-1820-4f7e-96c7-7d2be80cd40e", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "41097ebb-8bff-45f2-98d1-558df3ad3db9", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "af83e659-8201-4f48-bee4-280d98fdc06e", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "c644bdb1-fcb2-4cdf-a237-d47cbeb97710", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "8895e588-9a42-43ff-95ab-70d3099b9fb1", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "bb573bf3-7635-4f05-9e11-0c126535da90", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "9c4688dc-06c4-4a07-a1c1-6d05263b471b", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-realm", + "view-identity-providers", + "query-users", + "manage-events", + "manage-users", + "manage-clients", + "create-client", + "impersonation", + "manage-authorization", + "view-users", + "view-events", + "view-clients", + "query-groups", + "query-realms", + "manage-identity-providers", + "query-clients", + "view-realm", + "view-authorization" + ] + } + }, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "b2b8574a-2172-4207-bd66-81d3e606f81b", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "2e9cc3c5-b4f5-419e-9f45-8183de010e4a", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "7ed0a086-8e7f-4356-964a-97c6c2f3b392", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "ef18d920-437b-4c50-a77a-d52ba65a5d0c", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + }, + { + "id": "221f6cb1-1153-489b-9576-013256e9bd24", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "attributes": {} + } + ], + "grafana-oauth": [ + { + "id": "d5eec91b-e640-4272-9913-26a58cc1f9fb", + "name": "viewer", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", + "attributes": {} + }, + { + "id": "90689d48-cf66-4447-a32e-af5b43d5d99b", + "name": "editor", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", + "attributes": {} + }, + { + "id": "d496cbaa-dd46-4dde-a979-1e03c6a5327f", + "name": "admin", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "df048369-fdc7-4140-96c5-01289ded7dbb", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "7670e24f-84f7-4f8c-8893-235a773ebc92", + "attributes": {} + } + ], + "master-realm": [ + { + "id": "1c6bc438-9e0e-4dc8-b47d-e7349f939f88", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "9a3a4e5a-a92a-4e14-9d7a-d404dc6a83ec", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "1df12dcf-3e7a-4605-a44e-44bd0471d100", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "bc7bd095-c9b7-4491-8534-814b68533e83", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "4100adf2-8799-4d9c-a2be-c7362fef7928", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "02ae9731-67ab-4538-9739-55d02d038a27", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "master-realm": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "d6d87c0d-ed3e-467f-9764-abe3e802ac7a", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "a3e892fa-25a5-41a4-84f0-236ed43a48fe", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "master-realm": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "7705334c-caa8-43c7-b2aa-1ba35bddbda1", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "6b2e8ba8-c65d-4d92-8cf8-d3a99248ae86", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "c29add33-e3f9-4fa1-a8c7-4705ffc80931", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "047cf77a-d64c-4599-b343-638c4484238c", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "2ddd3da7-2f35-4d7b-a6f8-0196bc355a88", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "b3876834-8ffc-4d1b-b858-cf432421d1a7", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "8ba0152b-b9dd-4cf4-a105-756e44790cf9", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "d7fd27db-d4ea-4698-9086-722a514ca6c2", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "8e3ec57d-3407-48ca-8ffa-2c5e2ee3e5a8", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + }, + { + "id": "f5ee050e-057e-4c2b-8af1-24597d9b6cf1", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "attributes": {} + } + ], + "account": [ + { + "id": "8b61ce62-955f-4ef8-9aca-838ba4ae692c", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "dfc4feae-7f4d-49d9-a8a6-ebc8daf9e03c", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "0c9de20b-33f4-46ed-bbdf-82278e276ef9", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "03711ce3-f1bb-4ffb-8992-3bd49e77a823", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "1a2e579e-caf1-4377-80bd-6f247525b9a8", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "7b0f20ee-e51e-4dfa-ab66-afc2ecf15956", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "f3d4d78e-6fa7-4b75-a859-9a00beac44e6", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + }, + { + "id": "3090becf-b06f-44a9-8bd5-4cb977b2b8c8", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "5ee103d6-29d7-4223-a9c5-602c7252bf95", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256", + "RS256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256", + "RS256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "5b977d08-e097-4d01-bc86-c214715781eb", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/master/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "a6153b4f-515d-4269-86e2-cf98b04202e0", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/master/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "5316f2cf-967a-4c16-922c-a42f77874f8e", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "f88c8b93-be07-4e39-9262-eb10dff0865e", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "7670e24f-84f7-4f8c-8893-235a773ebc92", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", + "clientId": "grafana-oauth", + "name": "grafana-oauth", + "description": "Client for Grafana.", + "rootUrl": "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}", + "adminUrl": "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}", + "baseUrl": "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "{{ (lookup "v1" "Secret" .Release.Namespace "keycloak-grafana-client").data.client-secret | b64dec }}", + "redirectUris": [ + "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}/login/generic_oauth" + ], + "webOrigins": [ + "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1741002345", + "backchannel.logout.session.required": "true", + "frontchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", + "clientId": "master-realm", + "name": "master Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "fafcc9f5-8558-4656-8c0b-57a543258bb4", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [], + "optionalClientScopes": [] + }, + { + "id": "a5609fe5-ac89-4b11-ad66-b38486be51d3", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/master/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/master/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "1b3785a8-c5bd-4460-a745-d67e909a5d82", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "f8253b9c-004c-4ead-bd4c-ad753a0714cb", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "df29b12d-3315-461e-8e7b-3d935aa228d1", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "9d8136eb-b04e-4550-8c49-c957b639353f", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "71d6883a-ddcb-4379-a63c-e8c83bf02cf3", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "55341b99-a9b5-4779-ae24-cdf7c2078972", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "bcb0173a-ea58-4f58-b948-e7c401c4126c", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "d406d5dc-9b5b-4c8e-b5f2-61b0f718be4b", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "40ae6e10-7fc2-4c3e-8a7c-921a0d19be91", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "f963c16c-bded-42ec-b57d-b0881169ae82", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "fc52d93a-df2d-42e3-802d-c0009f3d3fd0", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "b4c7c94e-3340-4495-981f-ee5179df9166", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "72a6bd2f-5265-41cf-9ea9-44cb8706db2a", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "f70d7b79-ee75-4329-996c-1ffd3fe1f59b", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "d5c60f2f-6dc8-4134-932c-b2b95b5c30cc", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "e41e80a6-3634-40eb-9926-4d13e5de89dc", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "c6c356dc-8f0d-4c0c-adb8-c766b3e4ec9b", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "b3983402-f913-4e69-a7bb-384f2c9d4c3d", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "9f56afc2-a3f5-46ed-8b67-cf102a8dd888", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b642c58a-d827-4892-91c2-04c9aaf50dcc", + "name": "saml_organization", + "description": "Organization Membership", + "protocol": "saml", + "attributes": { + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "ae673550-75ed-4fa9-8da2-02b6917e2339", + "name": "organization", + "protocol": "saml", + "protocolMapper": "saml-organization-membership-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "0f062cfe-01b1-451f-93b3-5b41cfbe15a9", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6b5371ff-1612-4325-a663-c586a02868a6", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "bd0e5f6a-a1d0-4db2-9627-057b5c9077bf", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "65f7f982-1a39-4051-9683-b33bff9f44f2", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "8eebc960-6937-42d4-b451-3fa6bee17137", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "76aa098d-406d-43f5-9c03-a505f2a2a92a", + "name": "organization", + "description": "Additional claims about the organization a subject belongs to", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${organizationScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e3f20fa5-2d6e-4221-a716-70ec0a1b0ad2", + "name": "organization", + "protocol": "openid-connect", + "protocolMapper": "oidc-organization-membership-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "organization", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "00efe084-99f2-4ccb-9f34-8d79da93ff92", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "49f15869-90b4-4969-9184-3c2baf6c0b4f", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "17f927e9-2ef5-4c5e-8897-04acbca46963", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "3eb830a4-68ac-4546-8874-f88fb05fcf50", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "4936bac4-adcc-4409-b849-656715f7ed24", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "524f3242-2512-4b2c-9f05-2a9446d85ec2", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "bb0f4aba-3592-455c-8ac2-5422c3a5d888", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "cdb50046-cac1-435f-8133-6254738d1243", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "bb87aa49-8c20-4e75-a982-e7cb9f8c74a8", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "719b28fd-ec26-452f-ba54-0b31d9163393", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "51af0527-73ae-4814-86eb-6fdc893eaae3", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "71ce8520-6883-424f-84b8-74588a876466", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "3adde448-33b8-4751-8c99-f11706dced41", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "430b1d20-7001-4b92-bdf9-2ebd64f4a18b", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "3df5b00a-9d76-49b8-ad28-3b9d3ae49929", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "fb652af2-7cab-43d2-b76c-2c3179dedc0d", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + }, + { + "id": "c6b473cf-492f-4a4c-8a53-96d9aea35120", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "ba0fc791-e0a5-4586-b1c6-e6275deda54b", + "name": "service_account", + "description": "Specific scope for a client enabled for service accounts", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "dc217f85-8e1a-4738-96b4-b354e61ae4d1", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "d4cf4a9b-f5b2-4e40-89c0-bd4470b7d72c", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "83253ae8-4458-4534-b102-f09ddfa0d8e9", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "saml_organization", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt", + "organization" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "ab4a61c4-4c76-48dd-8b1f-8677da7295c5", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "5b82c93c-e47a-4db3-a884-d8be43e726a8", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper" + ] + } + }, + { + "id": "8948ed4f-b559-418a-b0d2-c266ce407b2c", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "2b440bb8-cb08-4e41-bdb3-28e1db17e45f", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "bb43f8ca-9546-47e1-8181-769adbb8da00", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "a623fae4-1631-48ca-86c6-d08c2ff19bbb", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper" + ] + } + }, + { + "id": "4652441c-56f9-4460-9bf7-4e6786e3f6b4", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "39cd9315-e204-4ee0-8e99-6bc18dc035a8", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "443f1e07-ae38-4369-aec6-250cf162279b", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" + ] + } + } + ], + "org.keycloak.storage.UserStorageProvider": [ + { + "id": "237e7955-1144-4130-a9a0-ec324e1725ab", + "name": "Kerberos", + "providerId": "kerberos", + "subComponents": {}, + "config": { + "serverPrincipal": [ + "HTTP/openid.{{.Values.acs.baseUrl}}@{{.Values.acs.baseUrl | upper}}" + ], + "allowPasswordAuthentication": [ + "false" + ], + "debug": [ + "false" + ], + "keyTab": [ + "/etc/krb5.keytab" + ], + "cachePolicy": [ + "DEFAULT" + ], + "updateProfileFirstLogin": [ + "false" + ], + "kerberosRealm": [ + "{{.Values.acs.baseUrl | upper}}" + ], + "enabled": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "846e15f7-4d01-4068-9f2e-752010aea31b", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "6483d5c2-7647-4537-b6f7-5eb458e7fe19", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "63b6e08f-f1f6-4e6f-a33e-4b2d016a7c65", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "b10c7ba5-57a4-451f-ab7c-50664cb99afb", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "51fe315f-0d23-46c9-9d89-3252308ee143", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "dbd8b458-f1f4-4370-872c-a02e825a02e9", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c3ea8bcb-1ded-44b3-9a43-5f8f990b3544", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "98416849-06dd-4252-8e6c-56986ecf2246", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "f527f4a0-e651-4f61-8e25-9ec7a1757b2e", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "97621baa-414a-4a64-99cd-a96964332c25", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c9408d57-cf65-424f-95ef-e1862f50929b", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "2c2e9e61-45ce-4ecc-a066-fe27f92bdbf3", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "fc8d6185-c116-458e-9934-0c054c3430a2", + "alias": "browser", + "description": "Browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "7687009d-82aa-46f3-8794-acc455208d3e", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "bc7aa4be-d745-49b6-84b3-01da5d90e1a6", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "f6c7be3b-079c-4780-aefe-25e8ad2fb5c3", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "db92fe11-579a-4c61-9516-75c2dc55fe1d", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "eab99805-ecfb-470c-b370-9d04de971988", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "c2f78d1a-90da-4242-8255-edd497dd06fb", + "alias": "registration", + "description": "Registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "2c6d7895-9cbc-4e5a-a3cd-fd20c5af693d", + "alias": "registration form", + "description": "Registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b1687032-c714-4f50-94f3-c2c7d145d6ea", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "291850d9-e1ba-4365-9978-8fb25135bcb0", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "73b4370e-6da3-4e80-b285-33b75187b14e", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "9d319b8d-dc49-4fe2-98b7-2623c7c4e8b9", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "26.1.1", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "verifiableCredentialsEnabled": false, + "adminPermissionsEnabled": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } + } +{{- end }} diff --git a/deploy/templates/openid/secrets.yaml b/deploy/templates/openid/secrets.yaml new file mode 100644 index 000000000..bc5b03111 --- /dev/null +++ b/deploy/templates/openid/secrets.yaml @@ -0,0 +1,14 @@ +{{- if .Values.openid.enabled }} +{{- if not (lookup "v1" "Secret" .Release.Namespace "keycloak-grafana-client") }} +apiVersion: v1 +kind: Secret +metadata: + name: "keycloak-grafana-client" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/resource-policy": "keep" +type: Opaque +data: + client-secret: {{ (randAlphaNum 32 | b64enc) | quote }} +{{- end }} +{{- end }} From cb867cabce4a16a09b2a5f3dc4c506b817456765 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Mon, 17 Mar 2025 00:37:51 +0000 Subject: [PATCH 04/62] Add oauth to Grafana config --- deploy/values.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/deploy/values.yaml b/deploy/values.yaml index 099962bbb..287b49d6e 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -345,6 +345,16 @@ grafana: header_name: X-Auth-Principal header_property: username auto_sign_up: true + auth.generic_oauth: + enabled: true + name: Keycloak + allow_sign_up: true + client_id: grafana-oauth + client_secret: {{ (lookup "v1" "Secret" .Release.Namespace "keycloak-grafana-client").data.client-secret | b64dec }} + scopes: openid profile email + auth_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/auth + token_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/token + api_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/userinfo sidecar: datasources: enabled: true From 614a94ea747a443b19070bf9cc930dd98a11fd34 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Mon, 17 Mar 2025 00:49:35 +0000 Subject: [PATCH 05/62] Add keytab volume --- deploy/templates/openid/openid.yaml | 2 ++ deploy/templates/openid/realm-config.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index dde945f94..da7fd8b61 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -56,6 +56,8 @@ spec: readOnly: true - name: krb5-conf mountPath: /config/krb5-conf + - name: krb5-keytab + mountPath: /etc/keytabs volumes: - name: realm-config configMap: diff --git a/deploy/templates/openid/realm-config.yaml b/deploy/templates/openid/realm-config.yaml index a5a72e755..0a92855d8 100644 --- a/deploy/templates/openid/realm-config.yaml +++ b/deploy/templates/openid/realm-config.yaml @@ -1994,7 +1994,7 @@ data: "false" ], "keyTab": [ - "/etc/krb5.keytab" + "/etc/keytabs/krb5.keytab" ], "cachePolicy": [ "DEFAULT" From 8d0668b1358fafbc5cefe7c413c63554ad6d2f40 Mon Sep 17 00:00:00 2001 From: D J Newbould Date: Fri, 21 Mar 2025 09:32:29 +0000 Subject: [PATCH 06/62] Add requested PR changes Used local secret resource to remove secret lookup. Moved helm templating out of the values file. --- deploy/crds/local-secret.yaml | 51 +++++++++++++++++++ deploy/templates/grafana/grafani-ini.yaml | 9 ++++ deploy/templates/openid/local-secrets.yaml | 11 ++++ .../templates/openid/openid-admin-user.yaml | 17 ------- deploy/templates/openid/openid.yaml | 9 ++-- deploy/templates/openid/realm-config.yaml | 2 +- deploy/templates/openid/secrets.yaml | 14 ----- deploy/values.yaml | 25 +++++++-- 8 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 deploy/crds/local-secret.yaml create mode 100644 deploy/templates/grafana/grafani-ini.yaml create mode 100644 deploy/templates/openid/local-secrets.yaml delete mode 100644 deploy/templates/openid/openid-admin-user.yaml delete mode 100644 deploy/templates/openid/secrets.yaml diff --git a/deploy/crds/local-secret.yaml b/deploy/crds/local-secret.yaml new file mode 100644 index 000000000..d2a142b44 --- /dev/null +++ b/deploy/crds/local-secret.yaml @@ -0,0 +1,51 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: localsecrets.factoryplus.app.amrc.co.uk +spec: + group: factoryplus.app.amrc.co.uk + names: + kind: LocalSecret + plural: localsecrets + categories: + - all + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: Secret + jsonPath: ".spec.secret" + type: string + - name: Key + jsonPath: ".spec.key" + type: string + - name: Format + jsonPath: ".spec.format" + type: string + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: [secret, key, format] + properties: + secret: + description: The name of the Secret to edit. + type: string + key: + description: The key to create within the Secret. + type: string + format: + description: > + The format of the secret value. Currently must be Password. + type: string + enum: [Password] + status: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} diff --git a/deploy/templates/grafana/grafani-ini.yaml b/deploy/templates/grafana/grafani-ini.yaml new file mode 100644 index 000000000..60fbaa41f --- /dev/null +++ b/deploy/templates/grafana/grafani-ini.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: {{ .Release.Namespace }} + name: acs-grafana-config +data: + auth_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/auth + token_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/token + api_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/userinfo \ No newline at end of file diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml new file mode 100644 index 000000000..38a8b04ae --- /dev/null +++ b/deploy/templates/openid/local-secrets.yaml @@ -0,0 +1,11 @@ +{{if .Values.openid.enabled}} +apiVersion: factoryplus.app.amrc.co.uk/v1 +kind: LocalSecret +metadata: + namespace: {{ .Release.Namespace }} + name: "keycloak-grafana-client" +spec: + format: Password + secret: "keycloak-grafana-client" + key: "grafana-oauth" +{{end }} \ No newline at end of file diff --git a/deploy/templates/openid/openid-admin-user.yaml b/deploy/templates/openid/openid-admin-user.yaml deleted file mode 100644 index 6f318aa32..000000000 --- a/deploy/templates/openid/openid-admin-user.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if .Values.openid.enabled }} -{{- if not (lookup "v1" "Secret" .Release.Namespace "openid-admin-user") }} - -apiVersion: v1 -kind: Secret -metadata: - name: "openid-admin-user" - namespace: {{ .Release.Namespace }} - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: - username: {{ (printf "admin@%s" (.Values.identity.realm | required "values.identity.realm is required!") | b64enc) | quote }} - password: {{ (printf "" | b64enc) | quote }} - -{{- end }} -{{- end -}} diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index da7fd8b61..1e17f8201 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -28,14 +28,11 @@ spec: args: ["start-dev", "--import-realm"] env: - name: KEYCLOAK_ADMIN - valueFrom: - secretKeyRef: - name: openid-admin-user - key: username + value: "admin" - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: secretKeyRef: - name: openid-admin-user + name: admin-password key: password - name: KC_PROXY value: "edge" @@ -65,7 +62,7 @@ spec: - name: krb5-conf configMap: name: krb5-conf - - name: krb5-keytabs + - name: krb5-keytab secret: secretName: krb5-keytabs items: diff --git a/deploy/templates/openid/realm-config.yaml b/deploy/templates/openid/realm-config.yaml index 0a92855d8..2659e8919 100644 --- a/deploy/templates/openid/realm-config.yaml +++ b/deploy/templates/openid/realm-config.yaml @@ -944,7 +944,7 @@ data: "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "{{ (lookup "v1" "Secret" .Release.Namespace "keycloak-grafana-client").data.client-secret | b64dec }}", + "secret": "$__file{/etc/secrets/auth_generic_oauth/client_secret", "redirectUris": [ "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}/login/generic_oauth" ], diff --git a/deploy/templates/openid/secrets.yaml b/deploy/templates/openid/secrets.yaml deleted file mode 100644 index bc5b03111..000000000 --- a/deploy/templates/openid/secrets.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- if .Values.openid.enabled }} -{{- if not (lookup "v1" "Secret" .Release.Namespace "keycloak-grafana-client") }} -apiVersion: v1 -kind: Secret -metadata: - name: "keycloak-grafana-client" - namespace: {{ .Release.Namespace }} - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: - client-secret: {{ (randAlphaNum 32 | b64enc) | quote }} -{{- end }} -{{- end }} diff --git a/deploy/values.yaml b/deploy/values.yaml index 287b49d6e..e5fb533dd 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -350,11 +350,28 @@ grafana: name: Keycloak allow_sign_up: true client_id: grafana-oauth - client_secret: {{ (lookup "v1" "Secret" .Release.Namespace "keycloak-grafana-client").data.client-secret | b64dec }} + client_secret: $__file{/etc/secrets/auth_generic_oauth/client_secret} scopes: openid profile email - auth_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/auth - token_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/token - api_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/userinfo + auth_url: $__file{/etc/acs-config/data/auth_url} + token_url: $__file{/etc/acs-config/data/token_url} + api_url: $__file{/etc/acs-config/data/api_url} + extraConfigmapMounts: + - name: ini-config + mountPath: /etc/acs-config/ + configMap: acs-grafana-config + readOnly: true + optional: false + + extraSecretMounts: + - name: keycloak-grafana-client + secretName: keycloak-grafana-client + defaultMode: 0440 + mountPath: /etc/secrets/auth_generic_oauth + readOnly: true + extraInitContainers: + - name: mountsecret + image: busybox:latest + command: [ ] sidecar: datasources: enabled: true From 0d0ea8333a2ee5aa53ea4197e829f93c6a800c4a Mon Sep 17 00:00:00 2001 From: D J Newbould Date: Fri, 21 Mar 2025 13:43:22 +0000 Subject: [PATCH 07/62] Add local secret grafana mount --- deploy/templates/hooks/post-delete.yaml | 3 ++- deploy/templates/identity/rbac.yaml | 4 ++-- deploy/templates/openid/local-secrets.yaml | 2 +- deploy/templates/openid/openid.yaml | 27 ++++++++++++++++++++++ deploy/values.yaml | 16 +++++-------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/deploy/templates/hooks/post-delete.yaml b/deploy/templates/hooks/post-delete.yaml index d3d8ebb87..7c2da7775 100644 --- a/deploy/templates/hooks/post-delete.yaml +++ b/deploy/templates/hooks/post-delete.yaml @@ -18,6 +18,7 @@ spec: - | echo "Starting cleanup..." for i in $(kubectl -n {{ .Release.Namespace }} get kerberos-keys -o name); do kubectl -n {{ .Release.Namespace }} patch $i --type=json -p='[{"op": "remove", "path": "/metadata/finalizers"}]'; done + for i in $(kubectl -n {{ .Release.Namespace }} get localsecrets -o name); do kubectl -n {{ .Release.Namespace }} patch $i --type=json -p='[{"op": "remove", "path": "/metadata/finalizers"}]'; done echo "Cleanup complete!" restartPolicy: Never backoffLimit: 3 @@ -45,7 +46,7 @@ metadata: "helm.sh/hook-delete-policy": before-hook-creation rules: - apiGroups: [ "factoryplus.app.amrc.co.uk" ] - resources: [ "kerberos-keys" ] + resources: [ "kerberos-keys", "localsecrets" ] verbs: [ "get", "list", "patch" ] # Specify only necessary actions --- diff --git a/deploy/templates/identity/rbac.yaml b/deploy/templates/identity/rbac.yaml index 26b425024..30b82e01e 100644 --- a/deploy/templates/identity/rbac.yaml +++ b/deploy/templates/identity/rbac.yaml @@ -13,10 +13,10 @@ metadata: name: krb-keys-operator rules: - apiGroups: [factoryplus.app.amrc.co.uk] - resources: [kerberos-keys] + resources: [kerberos-keys, localsecrets] verbs: [list, get, watch, patch] - apiGroups: [factoryplus.app.amrc.co.uk] - resources: [kerberos-keys/status] + resources: [kerberos-keys/status, localsecrets/status] verbs: [list, get, create, update, delete, watch, patch] - apiGroups: [""] resources: [secrets] diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml index 38a8b04ae..92e1243a2 100644 --- a/deploy/templates/openid/local-secrets.yaml +++ b/deploy/templates/openid/local-secrets.yaml @@ -7,5 +7,5 @@ metadata: spec: format: Password secret: "keycloak-grafana-client" - key: "grafana-oauth" + key: "client_secret" {{end }} \ No newline at end of file diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index 1e17f8201..552381ede 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -83,4 +83,31 @@ spec: targetPort: 8080 selector: factory-plus.service: openid + +--- + +apiVersion: factoryplus.app.amrc.co.uk/v1 +kind: KerberosKey +metadata: + name: sv1openid + namespace: {{ .Release.Namespace }} +spec: + type: Random + principal: sv1openid@{{ .Values.identity.realm | required "values.identity.realm is required!" }} + secret: openid-keytabs/client + + +--- + +apiVersion: factoryplus.app.amrc.co.uk/v1 +kind: KerberosKey +metadata: + name: http.openid + namespace: {{ .Release.Namespace }} +spec: + type: Random + principal: HTTP/openid.{{ .Release.Namespace }}.svc.cluster.local@{{ .Values.identity.realm | required "values.identity.realm is required!" }} + additionalPrincipals: + - HTTP/openid.{{.Values.acs.baseUrl | required "values.acs.baseUrl is required"}}@{{ .Values.identity.realm | required "values.identity.realm is required!" }} + secret: openid-keytabs/server {{- end }} diff --git a/deploy/values.yaml b/deploy/values.yaml index e5fb533dd..56b2ddf5b 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -352,26 +352,22 @@ grafana: client_id: grafana-oauth client_secret: $__file{/etc/secrets/auth_generic_oauth/client_secret} scopes: openid profile email - auth_url: $__file{/etc/acs-config/data/auth_url} - token_url: $__file{/etc/acs-config/data/token_url} - api_url: $__file{/etc/acs-config/data/api_url} + auth_url: $__file{/etc/acs-config/auth_url} + token_url: $__file{/etc/acs-config/token_url} + api_url: $__file{/etc/acs-config/api_url} extraConfigmapMounts: - name: ini-config - mountPath: /etc/acs-config/ + mountPath: /etc/acs-config configMap: acs-grafana-config + subPath: "" readOnly: true optional: false - extraSecretMounts: - - name: keycloak-grafana-client + - name: keycloak-grafana-client-mount secretName: keycloak-grafana-client defaultMode: 0440 mountPath: /etc/secrets/auth_generic_oauth readOnly: true - extraInitContainers: - - name: mountsecret - image: busybox:latest - command: [ ] sidecar: datasources: enabled: true From 127c69230681c81381ae9f3156be5e17ee483900 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 10:14:53 +0000 Subject: [PATCH 08/62] Add realm creation to service-setup --- acs-service-setup/lib/openid-realm.js | 205 +++++++++++++++++++++++++ acs-service-setup/lib/service-setup.js | 4 + 2 files changed, 209 insertions(+) create mode 100644 acs-service-setup/lib/openid-realm.js diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js new file mode 100644 index 000000000..34c9ea510 --- /dev/null +++ b/acs-service-setup/lib/openid-realm.js @@ -0,0 +1,205 @@ +import crypto from "crypto"; +import { URLSearchParams } from "url"; + +/** + * Create the startup OpenID realm `factory_plus`. + * + * @async + * @param {ServiceSetup} service_setup - Contains the configuration for setting up the realm. + * @returns {Promise} Resolves when the realm creation process finishes. + */ +export async function create_openid_realm(service_setup) { + new RealmSetup(service_setup).run(); +} + +class RealmSetup { + constructor(service_setup) { + const { fplus } = service_setup; + this.log = fplus.debug.bound("oauth"); + + this.username = fplus.opts.username; + this.password = fplus.opts.password; + this.realm = "factory_plus"; + this.base_url = fplus.acs_config.base_url; + this.access_token = ""; + this.refresh_token = ""; + } + + /** + * Run setup for the realm. This generates the full realm representation. + * + * A basic realm is created first and then populated with clients. + * + * @async + * @returns {Promise} + */ + async run() { + let base_realm = { + id: crypto.randomUUID(), + realm: this.realm, + displayName: "Factory+", + displayNameHtml: '
Factory+
', + enabled: true, + components: { + "org.keycloak.storage.UserStorageProvider": [ + { + id: crypto.randomUUID(), + name: "Kerberos", + providerId: "kerberos", + subComponents: {}, + config: { + serverPrincipal: [ + `HTTP/openid.${this.base_url}@${this.base_url.toUpperCase()}`, + ], + allowPasswordAuthentication: ["false"], + debug: ["true"], + keyTab: ["/etc/keytabs/krb5.keytab"], + cachePolicy: ["DEFAULT"], + updateProfileFirstLogin: ["false"], + kerberosRealm: [this.base_url.toUpperCase()], + enabled: ["true"], + }, + }, + ], + }, + }; + + await this.get_initial_access_token(); + await this.create_basic_realm(base_realm, false); + + const client_configs = Object.values(fplus.config.openidClients); + client_configs + .filter((client) => client.enabled === true) + .forEach((client) => this.create_client(client, false)); + } + + /** + * Create a new realm by POSTing to the OpenID service. Throws if the response is not ok. + * + * @async + * @param {Object} realm_representation - An object containing all the values that should be created for the realm. + * @param {Boolean} is_retry - Whether the request is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. + * @returns {Promise} Resolves when the realm is created. + */ + async create_basic_realm(realm_representation, is_retry) { + const realm_url = `openid.${this.base_url}/admin/realms`; + + try { + const response = await fetch(realm_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.access_token}`, + }, + body: JSON.stringify(realm_representation), + }); + + if (!response.ok) { + const status = response.status(); + + if (status == 401 && !retry) { + await this.get_initial_access_token(this.refresh_token); + await this.create_basic_realm(client_representation, true); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } catch (error) { + this.log(`Couldn't setup realm: ${error}`); + } + } + + /** + * Create a new client by POSTing to the OpenID service. Throws if the response is not ok. + * + * @async + * @param {Object} client_representation - An object containing all the values that should be created for the client. + * @param {Boolean} is_retry - Whether the request is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. + * @returns {Promise} Resolves when the client is created. + */ + async create_client(client_representation, is_retry) { + // Make sure an ID is present + if (client_representation.id == undefined) { + client_representation.id == crypto.randomUUID(); + } + + const client_url = `openid.${this.base_url}/admin/realms/${this.realm}/clients`; + + try { + const response = await fetch(client_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.access_token}`, + }, + body: JSON.stringify(client_representation), + }); + + if (!response.ok) { + const status = response.status(); + + if (status == 401 && !retry) { + await this.get_initial_access_token(this.refresh_token); + await this.create_client(client_representation, true); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } catch (error) { + this.log(`Couldn't setup client: ${error}`); + } + } + + /** + * Fetch an initial access token for the user in the specified realm. This should only be used during realm setup. + * + * Sets the `access_token` and `refresh_token` properties of `OAuthRealm` as a side effect. + * + * @async + * @param {string | undefined} [refresh_token] - Optional refresh token. If this is passed we use it with bearer auth. + * @returns {Promise<[string, string]} Resolves to an access token and a refresh token. + */ + async get_initial_access_token(refresh_token) { + const token_url = `openid.${this.base_url}/realms/${this.realm}/protocol/openid-connect/token`; + + const params = new URLSearchParams(); + if (refresh_token != undefined) { + params.append("grant_type", "refresh_token"); + params.append("client_id", "admin-cli"); + params.append("refresh_token", refresh_token); + } else { + params.append("grant_type", "password"); + params.append("client_id", "admin-cli"); + params.append("username", this.username); + params.append("password", this.password); + } + + try { + const response = await fetch(token_url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + + if (response.ok) { + data = await response.json(); + this.access_token = data.access_token; + this.refresh_token = data.refresh_token; + + return [data.access_token, data.refresh_token]; + } else { + const status = response.status(); + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } catch (error) { + this.log( + `Couldn't get an initial access token for realm setup: ${error}`, + ); + } + } +} diff --git a/acs-service-setup/lib/service-setup.js b/acs-service-setup/lib/service-setup.js index 75ee6caa1..0ad5e47c9 100644 --- a/acs-service-setup/lib/service-setup.js +++ b/acs-service-setup/lib/service-setup.js @@ -10,6 +10,7 @@ import { DumpLoader } from "./dumps.js"; import { fixups } from "./fixups.js"; import { setup_git_repos } from "./git-repos.js"; import { setup_local_uuids } from "./local-uuids.js"; +import { create_openid_realm } from "./openid-realm.js"; export class ServiceSetup { constructor (opts) { @@ -58,6 +59,9 @@ export class ServiceSetup { this.log("Migrating legacy Auth groups"); await migrate_auth_groups(this); + this.log("Creating OpenID realm"); + await create_openid_realm(this); + this.log("Finished setup"); } } From f73f3b62e7a7811869700ac319f0e6ddbf82efe6 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 12:02:19 +0000 Subject: [PATCH 09/62] Add service lookup for provided client configs --- acs-service-setup/lib/openid-realm.js | 28 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 34c9ea510..c90d6557e 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -20,7 +20,8 @@ class RealmSetup { this.username = fplus.opts.username; this.password = fplus.opts.password; this.realm = "factory_plus"; - this.base_url = fplus.acs_config.base_url; + this.base_url = fplus.acs_config.domain; + this.secure = fplus.acs_config.secure; this.access_token = ""; this.refresh_token = ""; } @@ -82,7 +83,7 @@ class RealmSetup { * @returns {Promise} Resolves when the realm is created. */ async create_basic_realm(realm_representation, is_retry) { - const realm_url = `openid.${this.base_url}/admin/realms`; + const realm_url = `${this.secure}://openid.${this.base_url}/admin/realms`; try { const response = await fetch(realm_url, { @@ -97,7 +98,7 @@ class RealmSetup { if (!response.ok) { const status = response.status(); - if (status == 401 && !retry) { + if (status == 401 && !is_retry) { await this.get_initial_access_token(this.refresh_token); await this.create_basic_realm(client_representation, true); } else { @@ -119,12 +120,19 @@ class RealmSetup { * @returns {Promise} Resolves when the client is created. */ async create_client(client_representation, is_retry) { - // Make sure an ID is present - if (client_representation.id == undefined) { - client_representation.id == crypto.randomUUID(); + const client = { + id: crypto.randomUUID(), + clientId: client_representation.clientId, + name: client_representation.name, + rootUrl: `${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + adminUrl: `${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + baseUrl: `${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + enabled: true, + secret: client_representation.secret, + redirectUris: [`${this.secure}://${client_representation.name}.${this.base_url}${client.redirectPath}`], } - const client_url = `openid.${this.base_url}/admin/realms/${this.realm}/clients`; + const client_url = `${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients`; try { const response = await fetch(client_url, { @@ -133,13 +141,13 @@ class RealmSetup { "Content-Type": "application/json", Authorization: `Bearer ${this.access_token}`, }, - body: JSON.stringify(client_representation), + body: JSON.stringify(client), }); if (!response.ok) { const status = response.status(); - if (status == 401 && !retry) { + if (status == 401 && !is_retry) { await this.get_initial_access_token(this.refresh_token); await this.create_client(client_representation, true); } else { @@ -162,7 +170,7 @@ class RealmSetup { * @returns {Promise<[string, string]} Resolves to an access token and a refresh token. */ async get_initial_access_token(refresh_token) { - const token_url = `openid.${this.base_url}/realms/${this.realm}/protocol/openid-connect/token`; + const token_url = `${this.secure}://openid.${this.base_url}/realms/${this.realm}/protocol/openid-connect/token`; const params = new URLSearchParams(); if (refresh_token != undefined) { From da8c6687717f4d163fbeb204d6f090a938e181e7 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 13:01:59 +0000 Subject: [PATCH 10/62] Add Grafana client config values --- deploy/values.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/deploy/values.yaml b/deploy/values.yaml index 56b2ddf5b..2c378f705 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -123,6 +123,25 @@ serviceSetup: helmChart: # Chart to deploy an edge cluster #cluster: null + openidClients: + grafana: + enabled: true + clientId: "Grafana" + name: "Grafana" + redirectHost: "grafana" + redirectPath: "/login/generic_oauth" + secret: $__file{/etc/secrets/auth_generic_oauth/client_secret} + roles: + - name: "viewer" + - name: "editor" + - name: "admin" + extraSecretMounts: + - name: keycloak-grafana-client-mount + secretName: keycloak-grafana-client + defaultMode: 0440 + mountPath: /etc/secrets/auth_generic_oauth + readOnly: true + edgeHelm: enabled: true From dd65a241f6c69ccee3d0c59268d8f687f0880469 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 15:36:18 +0000 Subject: [PATCH 11/62] Fix keytab secret names --- deploy/templates/openid/openid.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index 552381ede..c474032e1 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -53,7 +53,7 @@ spec: readOnly: true - name: krb5-conf mountPath: /config/krb5-conf - - name: krb5-keytab + - name: openid-keytabs mountPath: /etc/keytabs volumes: - name: realm-config @@ -62,14 +62,14 @@ spec: - name: krb5-conf configMap: name: krb5-conf - - name: krb5-keytab + - name: openid-keytabs secret: - secretName: krb5-keytabs + secretName: openid-keytabs items: - path: client - key: sv1openid + key: client - path: server - key: http.openid + key: server --- apiVersion: v1 kind: Service From 4205c43189c7f4cf0de0511c338c0007b3298726 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 16:02:51 +0000 Subject: [PATCH 12/62] Add client_secret volume mount --- deploy/templates/openid/openid.yaml | 8 +- deploy/templates/openid/realm-config.yaml | 2740 --------------------- deploy/templates/service-setup.yaml | 10 +- deploy/values.yaml | 6 - 4 files changed, 9 insertions(+), 2755 deletions(-) delete mode 100644 deploy/templates/openid/realm-config.yaml diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index c474032e1..eac2dd9ec 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -25,7 +25,7 @@ spec: - name: openid image: "{{ .Values.openid.image.repository }}:{{ .Values.openid.image.tag }}" imagePullPolicy: {{ .Values.openid.image.pullPolicy }} - args: ["start-dev", "--import-realm"] + args: ["start-dev"] env: - name: KEYCLOAK_ADMIN value: "admin" @@ -48,17 +48,11 @@ spec: path: /health/ready port: 9000 volumeMounts: - - name: realm-config - mountPath: /opt/keycloak/data/import - readOnly: true - name: krb5-conf mountPath: /config/krb5-conf - name: openid-keytabs mountPath: /etc/keytabs volumes: - - name: realm-config - configMap: - name: keycloak-realm-config - name: krb5-conf configMap: name: krb5-conf diff --git a/deploy/templates/openid/realm-config.yaml b/deploy/templates/openid/realm-config.yaml deleted file mode 100644 index 2659e8919..000000000 --- a/deploy/templates/openid/realm-config.yaml +++ /dev/null @@ -1,2740 +0,0 @@ -{{- if .Values.openid.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: keycloak-realm-config - namespace: {{ .Release.Namespace }} -data: - realm-config.json: | - { - "id": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", - "realm": "factory_plus", - "displayName": "Factory+", - "displayNameHtml": "
Factory+
", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 60, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxTemporaryLockouts": 0, - "bruteForceStrategy": "MULTIPLE", - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "6e9a20eb-25fe-4c56-acd7-89dcf59246ac", - "name": "admin", - "description": "${role_admin}", - "composite": true, - "composites": { - "realm": [ - "create-realm" - ], - "client": { - "master-realm": [ - "view-realm", - "manage-realm", - "create-client", - "view-events", - "view-users", - "manage-identity-providers", - "view-clients", - "query-realms", - "manage-users", - "manage-clients", - "view-authorization", - "manage-authorization", - "manage-events", - "query-clients", - "impersonation", - "view-identity-providers", - "query-groups", - "query-users" - ] - } - }, - "clientRole": false, - "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", - "attributes": {} - }, - { - "id": "c47f84ec-e052-46a9-88bb-007b69f97500", - "name": "create-realm", - "description": "${role_create-realm}", - "composite": false, - "clientRole": false, - "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", - "attributes": {} - }, - { - "id": "02f9d24c-49f9-448d-b713-097cd865c156", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", - "attributes": {} - }, - { - "id": "5ee103d6-29d7-4223-a9c5-602c7252bf95", - "name": "default-roles-master", - "description": "${role_default-roles}", - "composite": true, - "composites": { - "realm": [ - "offline_access", - "uma_authorization" - ], - "client": { - "account": [ - "view-profile", - "manage-account" - ] - } - }, - "clientRole": false, - "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", - "attributes": {} - }, - { - "id": "d44417f0-73b3-46a3-b518-4aa287ac2a11", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1", - "attributes": {} - } - ], - "client": { - "realm-management": [ - { - "id": "003a91f9-6480-42d1-a059-11fc8042889c", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "8e09177a-8893-44a6-ac06-9c47e8c18318", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "0acc8e69-2ed7-4928-9162-5e818910292a", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "811e9a81-5d39-492e-8ff0-59742b5060c8", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "a02eacb5-ac1d-461a-8d95-6982985dfb90", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "91440da6-0d24-41c5-852f-b3efa66a6656", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "896539b3-4f55-4ff4-8fa0-8c87c022ed75", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "87b45e17-1820-4f7e-96c7-7d2be80cd40e", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "41097ebb-8bff-45f2-98d1-558df3ad3db9", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "af83e659-8201-4f48-bee4-280d98fdc06e", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-users", - "query-groups" - ] - } - }, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "c644bdb1-fcb2-4cdf-a237-d47cbeb97710", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "8895e588-9a42-43ff-95ab-70d3099b9fb1", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "bb573bf3-7635-4f05-9e11-0c126535da90", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "9c4688dc-06c4-4a07-a1c1-6d05263b471b", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "manage-realm", - "view-identity-providers", - "query-users", - "manage-events", - "manage-users", - "manage-clients", - "create-client", - "impersonation", - "manage-authorization", - "view-users", - "view-events", - "view-clients", - "query-groups", - "query-realms", - "manage-identity-providers", - "query-clients", - "view-realm", - "view-authorization" - ] - } - }, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "b2b8574a-2172-4207-bd66-81d3e606f81b", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "2e9cc3c5-b4f5-419e-9f45-8183de010e4a", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "7ed0a086-8e7f-4356-964a-97c6c2f3b392", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "ef18d920-437b-4c50-a77a-d52ba65a5d0c", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - }, - { - "id": "221f6cb1-1153-489b-9576-013256e9bd24", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "attributes": {} - } - ], - "grafana-oauth": [ - { - "id": "d5eec91b-e640-4272-9913-26a58cc1f9fb", - "name": "viewer", - "description": "", - "composite": false, - "clientRole": true, - "containerId": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", - "attributes": {} - }, - { - "id": "90689d48-cf66-4447-a32e-af5b43d5d99b", - "name": "editor", - "description": "", - "composite": false, - "clientRole": true, - "containerId": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", - "attributes": {} - }, - { - "id": "d496cbaa-dd46-4dde-a979-1e03c6a5327f", - "name": "admin", - "description": "", - "composite": false, - "clientRole": true, - "containerId": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", - "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "broker": [ - { - "id": "df048369-fdc7-4140-96c5-01289ded7dbb", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "7670e24f-84f7-4f8c-8893-235a773ebc92", - "attributes": {} - } - ], - "master-realm": [ - { - "id": "1c6bc438-9e0e-4dc8-b47d-e7349f939f88", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "9a3a4e5a-a92a-4e14-9d7a-d404dc6a83ec", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "1df12dcf-3e7a-4605-a44e-44bd0471d100", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "bc7bd095-c9b7-4491-8534-814b68533e83", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "4100adf2-8799-4d9c-a2be-c7362fef7928", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "02ae9731-67ab-4538-9739-55d02d038a27", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "master-realm": [ - "query-groups", - "query-users" - ] - } - }, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "d6d87c0d-ed3e-467f-9764-abe3e802ac7a", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "a3e892fa-25a5-41a4-84f0-236ed43a48fe", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "master-realm": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "7705334c-caa8-43c7-b2aa-1ba35bddbda1", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "6b2e8ba8-c65d-4d92-8cf8-d3a99248ae86", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "c29add33-e3f9-4fa1-a8c7-4705ffc80931", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "047cf77a-d64c-4599-b343-638c4484238c", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "2ddd3da7-2f35-4d7b-a6f8-0196bc355a88", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "b3876834-8ffc-4d1b-b858-cf432421d1a7", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "8ba0152b-b9dd-4cf4-a105-756e44790cf9", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "d7fd27db-d4ea-4698-9086-722a514ca6c2", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "8e3ec57d-3407-48ca-8ffa-2c5e2ee3e5a8", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - }, - { - "id": "f5ee050e-057e-4c2b-8af1-24597d9b6cf1", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "attributes": {} - } - ], - "account": [ - { - "id": "8b61ce62-955f-4ef8-9aca-838ba4ae692c", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "dfc4feae-7f4d-49d9-a8a6-ebc8daf9e03c", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "0c9de20b-33f4-46ed-bbdf-82278e276ef9", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "03711ce3-f1bb-4ffb-8992-3bd49e77a823", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": [ - "view-consent" - ] - } - }, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "1a2e579e-caf1-4377-80bd-6f247525b9a8", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "7b0f20ee-e51e-4dfa-ab66-afc2ecf15956", - "name": "view-groups", - "description": "${role_view-groups}", - "composite": false, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "f3d4d78e-6fa7-4b75-a859-9a00beac44e6", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": [ - "manage-account-links" - ] - } - }, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - }, - { - "id": "3090becf-b06f-44a9-8bd5-4cb977b2b8c8", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "5b977d08-e097-4d01-bc86-c214715781eb", - "attributes": {} - } - ] - } - }, - "groups": [], - "defaultRole": { - "id": "5ee103d6-29d7-4223-a9c5-602c7252bf95", - "name": "default-roles-master", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "ec42aa03-babd-4e32-9c4f-49f7d181c3d1" - }, - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpPolicyCodeReusable": false, - "otpSupportedApplications": [ - "totpAppFreeOTPName", - "totpAppGoogleName", - "totpAppMicrosoftAuthenticatorName" - ], - "localizationTexts": {}, - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256", - "RS256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyExtraOrigins": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256", - "RS256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "webAuthnPolicyPasswordlessExtraOrigins": [], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": [ - "manage-account", - "view-groups" - ] - } - ] - }, - "clients": [ - { - "id": "5b977d08-e097-4d01-bc86-c214715781eb", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/master/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/master/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - }, - { - "id": "a6153b4f-515d-4269-86e2-cf98b04202e0", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/master/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/master/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "5316f2cf-967a-4c16-922c-a42f77874f8e", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - }, - { - "id": "f88c8b93-be07-4e39-9262-eb10dff0865e", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "client.use.lightweight.access.token.enabled": "true", - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - }, - { - "id": "7670e24f-84f7-4f8c-8893-235a773ebc92", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "true", - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - }, - { - "id": "c196a3ad-6a90-4d52-bb4f-ad5d5c50d38f", - "clientId": "grafana-oauth", - "name": "grafana-oauth", - "description": "Client for Grafana.", - "rootUrl": "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}", - "adminUrl": "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}", - "baseUrl": "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "$__file{/etc/secrets/auth_generic_oauth/client_secret", - "redirectUris": [ - "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}/login/generic_oauth" - ], - "webOrigins": [ - "{{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "oidc.ciba.grant.enabled": "false", - "client.secret.creation.time": "1741002345", - "backchannel.logout.session.required": "true", - "frontchannel.logout.session.required": "true", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "display.on.consent.screen": "false", - "backchannel.logout.revoke.offline.tokens": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - }, - { - "id": "e7d96ab6-94b1-4fd2-91ac-d4f151b10a6b", - "clientId": "master-realm", - "name": "master Realm", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - }, - { - "id": "fafcc9f5-8558-4656-8c0b-57a543258bb4", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "true" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [], - "optionalClientScopes": [] - }, - { - "id": "a5609fe5-ac89-4b11-ad66-b38486be51d3", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/master/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/admin/master/console/*" - ], - "webOrigins": [ - "+" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "client.use.lightweight.access.token.enabled": "true", - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "1b3785a8-c5bd-4460-a745-d67e909a5d82", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "organization", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "f8253b9c-004c-4ead-bd4c-ad753a0714cb", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "df29b12d-3315-461e-8e7b-3d935aa228d1", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "9d8136eb-b04e-4550-8c49-c957b639353f", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "71d6883a-ddcb-4379-a63c-e8c83bf02cf3", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${profileScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "55341b99-a9b5-4779-ae24-cdf7c2078972", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "bcb0173a-ea58-4f58-b948-e7c401c4126c", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "d406d5dc-9b5b-4c8e-b5f2-61b0f718be4b", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "40ae6e10-7fc2-4c3e-8a7c-921a0d19be91", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "f963c16c-bded-42ec-b57d-b0881169ae82", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "fc52d93a-df2d-42e3-802d-c0009f3d3fd0", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "b4c7c94e-3340-4495-981f-ee5179df9166", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "72a6bd2f-5265-41cf-9ea9-44cb8706db2a", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "f70d7b79-ee75-4329-996c-1ffd3fe1f59b", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "d5c60f2f-6dc8-4134-932c-b2b95b5c30cc", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "e41e80a6-3634-40eb-9926-4d13e5de89dc", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "c6c356dc-8f0d-4c0c-adb8-c766b3e4ec9b", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "b3983402-f913-4e69-a7bb-384f2c9d4c3d", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "9f56afc2-a3f5-46ed-8b67-cf102a8dd888", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "b642c58a-d827-4892-91c2-04c9aaf50dcc", - "name": "saml_organization", - "description": "Organization Membership", - "protocol": "saml", - "attributes": { - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "ae673550-75ed-4fa9-8da2-02b6917e2339", - "name": "organization", - "protocol": "saml", - "protocolMapper": "saml-organization-membership-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "0f062cfe-01b1-451f-93b3-5b41cfbe15a9", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${emailScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "6b5371ff-1612-4325-a663-c586a02868a6", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - }, - { - "id": "bd0e5f6a-a1d0-4db2-9627-057b5c9077bf", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "65f7f982-1a39-4051-9683-b33bff9f44f2", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${addressScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "8eebc960-6937-42d4-b451-3fa6bee17137", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "introspection.token.claim": "true", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "76aa098d-406d-43f5-9c03-a505f2a2a92a", - "name": "organization", - "description": "Additional claims about the organization a subject belongs to", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${organizationScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "e3f20fa5-2d6e-4221-a716-70ec0a1b0ad2", - "name": "organization", - "protocol": "openid-connect", - "protocolMapper": "oidc-organization-membership-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "multivalued": "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "organization", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "00efe084-99f2-4ccb-9f34-8d79da93ff92", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "consent.screen.text": "", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "49f15869-90b4-4969-9184-3c2baf6c0b4f", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] - }, - { - "id": "17f927e9-2ef5-4c5e-8897-04acbca46963", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${phoneScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "3eb830a4-68ac-4546-8874-f88fb05fcf50", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "4936bac4-adcc-4409-b849-656715f7ed24", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "524f3242-2512-4b2c-9f05-2a9446d85ec2", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "consent.screen.text": "${rolesScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "bb0f4aba-3592-455c-8ac2-5422c3a5d888", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - }, - { - "id": "cdb50046-cac1-435f-8133-6254738d1243", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "introspection.token.claim": "true", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "bb87aa49-8c20-4e75-a982-e7cb9f8c74a8", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "introspection.token.claim": "true", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "id": "719b28fd-ec26-452f-ba54-0b31d9163393", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "51af0527-73ae-4814-86eb-6fdc893eaae3", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] - }, - { - "id": "71ce8520-6883-424f-84b8-74588a876466", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "3adde448-33b8-4751-8c99-f11706dced41", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - }, - { - "id": "430b1d20-7001-4b92-bdf9-2ebd64f4a18b", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "3df5b00a-9d76-49b8-ad28-3b9d3ae49929", - "name": "basic", - "description": "OpenID Connect scope for add all basic claims to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "fb652af2-7cab-43d2-b76c-2c3179dedc0d", - "name": "auth_time", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "AUTH_TIME", - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "auth_time", - "jsonType.label": "long" - } - }, - { - "id": "c6b473cf-492f-4a4c-8a53-96d9aea35120", - "name": "sub", - "protocol": "openid-connect", - "protocolMapper": "oidc-sub-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] - }, - { - "id": "ba0fc791-e0a5-4586-b1c6-e6275deda54b", - "name": "service_account", - "description": "Specific scope for a client enabled for service accounts", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "dc217f85-8e1a-4738-96b4-b354e61ae4d1", - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - }, - { - "id": "d4cf4a9b-f5b2-4e40-89c0-bd4470b7d72c", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - }, - { - "id": "83253ae8-4458-4534-b102-f09ddfa0d8e9", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "client_id", - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "client_id", - "jsonType.label": "String" - } - } - ] - } - ], - "defaultDefaultClientScopes": [ - "role_list", - "saml_organization", - "profile", - "email", - "roles", - "web-origins", - "acr", - "basic" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "address", - "phone", - "microprofile-jwt", - "organization" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "referrerPolicy": "no-referrer", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "ab4a61c4-4c76-48dd-8b1f-8677da7295c5", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "5b82c93c-e47a-4db3-a884-d8be43e726a8", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-usermodel-property-mapper", - "saml-role-list-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-full-name-mapper", - "oidc-address-mapper", - "saml-user-attribute-mapper" - ] - } - }, - { - "id": "8948ed4f-b559-418a-b0d2-c266ce407b2c", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "2b440bb8-cb08-4e41-bdb3-28e1db17e45f", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "bb43f8ca-9546-47e1-8181-769adbb8da00", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "a623fae4-1631-48ca-86c6-d08c2ff19bbb", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-usermodel-property-mapper", - "oidc-address-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper", - "saml-user-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-property-mapper", - "oidc-usermodel-attribute-mapper" - ] - } - }, - { - "id": "4652441c-56f9-4460-9bf7-4e6786e3f6b4", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "39cd9315-e204-4ee0-8e99-6bc18dc035a8", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "443f1e07-ae38-4369-aec6-250cf162279b", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": { - "kc.user.profile.config": [ - "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" - ] - } - } - ], - "org.keycloak.storage.UserStorageProvider": [ - { - "id": "237e7955-1144-4130-a9a0-ec324e1725ab", - "name": "Kerberos", - "providerId": "kerberos", - "subComponents": {}, - "config": { - "serverPrincipal": [ - "HTTP/openid.{{.Values.acs.baseUrl}}@{{.Values.acs.baseUrl | upper}}" - ], - "allowPasswordAuthentication": [ - "false" - ], - "debug": [ - "false" - ], - "keyTab": [ - "/etc/keytabs/krb5.keytab" - ], - "cachePolicy": [ - "DEFAULT" - ], - "updateProfileFirstLogin": [ - "false" - ], - "kerberosRealm": [ - "{{.Values.acs.baseUrl | upper}}" - ], - "enabled": [ - "true" - ] - } - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "846e15f7-4d01-4068-9f2e-752010aea31b", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - }, - { - "id": "6483d5c2-7647-4537-b6f7-5eb458e7fe19", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - }, - { - "id": "63b6e08f-f1f6-4e6f-a33e-4b2d016a7c65", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "RSA-OAEP" - ] - } - }, - { - "id": "b10c7ba5-57a4-451f-ab7c-50664cb99afb", - "name": "hmac-generated-hs512", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "HS512" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "51fe315f-0d23-46c9-9d89-3252308ee143", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "dbd8b458-f1f4-4370-872c-a02e825a02e9", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "c3ea8bcb-1ded-44b3-9a43-5f8f990b3544", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "98416849-06dd-4252-8e6c-56986ecf2246", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "f527f4a0-e651-4f61-8e25-9ec7a1757b2e", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "97621baa-414a-4a64-99cd-a96964332c25", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "c9408d57-cf65-424f-95ef-e1862f50929b", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "2c2e9e61-45ce-4ecc-a066-fe27f92bdbf3", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "fc8d6185-c116-458e-9934-0c054c3430a2", - "alias": "browser", - "description": "Browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "7687009d-82aa-46f3-8794-acc455208d3e", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "bc7aa4be-d745-49b6-84b3-01da5d90e1a6", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "f6c7be3b-079c-4780-aefe-25e8ad2fb5c3", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "db92fe11-579a-4c61-9516-75c2dc55fe1d", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "eab99805-ecfb-470c-b370-9d04de971988", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "c2f78d1a-90da-4242-8255-edd497dd06fb", - "alias": "registration", - "description": "Registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "2c6d7895-9cbc-4e5a-a3cd-fd20c5af693d", - "alias": "registration form", - "description": "Registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-terms-and-conditions", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 70, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "b1687032-c714-4f50-94f3-c2c7d145d6ea", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "291850d9-e1ba-4365-9978-8fb25135bcb0", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "73b4370e-6da3-4e80-b285-33b75187b14e", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "9d319b8d-dc49-4fe2-98b7-2623c7c4e8b9", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "TERMS_AND_CONDITIONS", - "name": "Terms and Conditions", - "providerId": "TERMS_AND_CONDITIONS", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} - }, - { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} - }, - { - "alias": "VERIFY_PROFILE", - "name": "Verify Profile", - "providerId": "VERIFY_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 90, - "config": {} - }, - { - "alias": "delete_credential", - "name": "Delete Credential", - "providerId": "delete_credential", - "enabled": true, - "defaultAction": false, - "priority": 100, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "firstBrokerLoginFlow": "first broker login", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaExpiresIn": "120", - "cibaAuthRequestedUserHint": "login_hint", - "oauth2DeviceCodeLifespan": "600", - "clientOfflineSessionMaxLifespan": "0", - "oauth2DevicePollingInterval": "5", - "clientSessionIdleTimeout": "0", - "parRequestUriLifespan": "60", - "clientSessionMaxLifespan": "0", - "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5", - "realmReusableOtpCode": "false" - }, - "keycloakVersion": "26.1.1", - "userManagedAccessAllowed": false, - "organizationsEnabled": false, - "verifiableCredentialsEnabled": false, - "adminPermissionsEnabled": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - } - } -{{- end }} diff --git a/deploy/templates/service-setup.yaml b/deploy/templates/service-setup.yaml index 110783af5..f3d8982ea 100644 --- a/deploy/templates/service-setup.yaml +++ b/deploy/templates/service-setup.yaml @@ -24,6 +24,9 @@ spec: name: krb5-conf - name: manager-ccache-storage emptyDir: { } + - name: grafana-client-secret + secret: + secretName: keycloak-grafana-client initContainers: - name: service-setup {{ include "amrc-connectivity-stack.image" (list . .Values.serviceSetup) | indent 10 }} @@ -53,12 +56,15 @@ spec: "secure" (.Values.acs.secure | ternary "s" "") "realm" .Values.identity.realm "directory" - (include "amrc-connectivity-stack.external-url" + (include "amrc-connectivity-stack.external-url" (list . "directory")) | toRawJson | quote }} volumeMounts: - mountPath: /data name: git-checkouts + - name: grafana-client-secret + mountPath: /etc/secret + readOnly: true - name: edge-helm-charts {{ include "amrc-connectivity-stack.image" (list . .Values.edgeHelm) | indent 10 }} env: @@ -78,7 +84,7 @@ spec: - name: manager image: "{{ include "amrc-connectivity-stack.image-name" (list . .Values.manager ) }}-backend" imagePullPolicy: {{ .Values.manager.image.pullPolicy }} - command: + command: - /bin/sh - "-c" - | diff --git a/deploy/values.yaml b/deploy/values.yaml index 2c378f705..cc609c253 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -135,12 +135,6 @@ serviceSetup: - name: "viewer" - name: "editor" - name: "admin" - extraSecretMounts: - - name: keycloak-grafana-client-mount - secretName: keycloak-grafana-client - defaultMode: 0440 - mountPath: /etc/secrets/auth_generic_oauth - readOnly: true edgeHelm: From 101fe91c5f2089c1cebf3475b3b420b2fdbd4c72 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 22:43:39 +0000 Subject: [PATCH 13/62] Fix config and add startup backoff --- acs-service-setup/lib/openid-realm.js | 69 +++++++++++++++++++-------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index c90d6557e..78e4d9574 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -9,7 +9,7 @@ import { URLSearchParams } from "url"; * @returns {Promise} Resolves when the realm creation process finishes. */ export async function create_openid_realm(service_setup) { - new RealmSetup(service_setup).run(); + await new RealmSetup(service_setup).run(); } class RealmSetup { @@ -20,10 +20,11 @@ class RealmSetup { this.username = fplus.opts.username; this.password = fplus.opts.password; this.realm = "factory_plus"; - this.base_url = fplus.acs_config.domain; - this.secure = fplus.acs_config.secure; + this.base_url = service_setup.acs_config.domain; + this.secure = service_setup.acs_config.secure; this.access_token = ""; this.refresh_token = ""; + this.config = service_setup.config; } /** @@ -54,7 +55,7 @@ class RealmSetup { ], allowPasswordAuthentication: ["false"], debug: ["true"], - keyTab: ["/etc/keytabs/krb5.keytab"], + keyTab: ["/etc/keytabs/server"], cachePolicy: ["DEFAULT"], updateProfileFirstLogin: ["false"], kerberosRealm: [this.base_url.toUpperCase()], @@ -68,10 +69,13 @@ class RealmSetup { await this.get_initial_access_token(); await this.create_basic_realm(base_realm, false); - const client_configs = Object.values(fplus.config.openidClients); - client_configs - .filter((client) => client.enabled === true) - .forEach((client) => this.create_client(client, false)); + const client_configs = Object.values(this.config.openidClients); + const enabled_clients = client_configs.filter( + (client) => client.enabled === true, + ); + for (const client of enabled_clients) { + await this.create_client(client, false); + } } /** @@ -83,7 +87,9 @@ class RealmSetup { * @returns {Promise} Resolves when the realm is created. */ async create_basic_realm(realm_representation, is_retry) { - const realm_url = `${this.secure}://openid.${this.base_url}/admin/realms`; + const realm_url = `http${this.secure}://openid.${this.base_url}/admin/realms`; + + this.log(`Attempting basic realm creation at: ${realm_url}`); try { const response = await fetch(realm_url, { @@ -96,11 +102,14 @@ class RealmSetup { }); if (!response.ok) { - const status = response.status(); + const status = response.status; if (status == 401 && !is_retry) { await this.get_initial_access_token(this.refresh_token); - await this.create_basic_realm(client_representation, true); + await this.create_basic_realm(realm_representation, true); + } else if (status == 503) { + await this.wait(10000); + await this.create_basic_realm(realm_representation, false); } else { const error = await response.text(); throw new Error(`${status}: ${error}`); @@ -124,15 +133,21 @@ class RealmSetup { id: crypto.randomUUID(), clientId: client_representation.clientId, name: client_representation.name, - rootUrl: `${this.secure}://${client_representation.redirectHost}.${this.base_url}`, - adminUrl: `${this.secure}://${client_representation.redirectHost}.${this.base_url}`, - baseUrl: `${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + rootUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + adminUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + baseUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, enabled: true, secret: client_representation.secret, - redirectUris: [`${this.secure}://${client_representation.name}.${this.base_url}${client.redirectPath}`], - } + redirectUris: [ + `http${this.secure}://${client_representation.redirectHost}.${this.base_url}${client_representation.redirectPath}`, + ], + }; + + const client_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients`; - const client_url = `${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients`; + this.log(`Attempting client creation at: ${client_url}`); + + this.log(`Client: ${client}`); try { const response = await fetch(client_url, { @@ -145,11 +160,14 @@ class RealmSetup { }); if (!response.ok) { - const status = response.status(); + const status = response.status; if (status == 401 && !is_retry) { await this.get_initial_access_token(this.refresh_token); await this.create_client(client_representation, true); + } else if (status == 503) { + await this.wait(10000); + await this.create_client(client_representation, false); } else { const error = await response.text(); throw new Error(`${status}: ${error}`); @@ -170,7 +188,9 @@ class RealmSetup { * @returns {Promise<[string, string]} Resolves to an access token and a refresh token. */ async get_initial_access_token(refresh_token) { - const token_url = `${this.secure}://openid.${this.base_url}/realms/${this.realm}/protocol/openid-connect/token`; + const token_url = `http${this.secure}://openid.${this.base_url}/realms/master/protocol/openid-connect/token`; + + this.log(`Attempting token request at: ${token_url}`); const params = new URLSearchParams(); if (refresh_token != undefined) { @@ -194,13 +214,16 @@ class RealmSetup { }); if (response.ok) { - data = await response.json(); + const data = await response.json(); this.access_token = data.access_token; this.refresh_token = data.refresh_token; return [data.access_token, data.refresh_token]; + } else if (response.status == 503) { + await this.wait(10000); + this.get_initial_access_token(); } else { - const status = response.status(); + const status = response.status; const error = await response.text(); throw new Error(`${status}: ${error}`); } @@ -210,4 +233,8 @@ class RealmSetup { ); } } + + async wait(milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); + } } From 72008679a5ebab468a564c5d7f92b8f384562449 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 23:51:06 +0000 Subject: [PATCH 14/62] Add generic client-secret fetching --- acs-service-setup/lib/openid-realm.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 78e4d9574..d9570099b 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -1,4 +1,6 @@ import crypto from "crypto"; +import fs from "fs/promises"; +import path from "path"; import { URLSearchParams } from "url"; /** @@ -129,6 +131,13 @@ class RealmSetup { * @returns {Promise} Resolves when the client is created. */ async create_client(client_representation, is_retry) { + const secret_path = path.join( + "/etc/secret", + client_representation.redirectHost, + ); + const content = await fs.readFile(secret_path, "utf8"); + const client_secret = content.trim(); + const client = { id: crypto.randomUUID(), clientId: client_representation.clientId, @@ -137,7 +146,7 @@ class RealmSetup { adminUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, baseUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, enabled: true, - secret: client_representation.secret, + secret: client_secret, redirectUris: [ `http${this.secure}://${client_representation.redirectHost}.${this.base_url}${client_representation.redirectPath}`, ], @@ -147,8 +156,6 @@ class RealmSetup { this.log(`Attempting client creation at: ${client_url}`); - this.log(`Client: ${client}`); - try { const response = await fetch(client_url, { method: "POST", From acdc62a451547c686247c81d581a63dd0bcb94c5 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 25 Mar 2025 23:54:22 +0000 Subject: [PATCH 15/62] Rename client-secret to be findable by service-setup --- deploy/templates/openid/local-secrets.yaml | 4 ++-- deploy/values.yaml | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml index 92e1243a2..94317ff4b 100644 --- a/deploy/templates/openid/local-secrets.yaml +++ b/deploy/templates/openid/local-secrets.yaml @@ -7,5 +7,5 @@ metadata: spec: format: Password secret: "keycloak-grafana-client" - key: "client_secret" -{{end }} \ No newline at end of file + key: "grafana" +{{end }} diff --git a/deploy/values.yaml b/deploy/values.yaml index cc609c253..a36f4662e 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -126,11 +126,10 @@ serviceSetup: openidClients: grafana: enabled: true - clientId: "Grafana" + clientId: "grafana" name: "Grafana" redirectHost: "grafana" redirectPath: "/login/generic_oauth" - secret: $__file{/etc/secrets/auth_generic_oauth/client_secret} roles: - name: "viewer" - name: "editor" From ac7c368f0dadde9c2791f43e18862615f9070f08 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 11:53:31 +0000 Subject: [PATCH 16/62] Fix misnamed grafana client ID --- deploy/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/values.yaml b/deploy/values.yaml index a36f4662e..72917efcb 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -361,7 +361,7 @@ grafana: enabled: true name: Keycloak allow_sign_up: true - client_id: grafana-oauth + client_id: grafana client_secret: $__file{/etc/secrets/auth_generic_oauth/client_secret} scopes: openid profile email auth_url: $__file{/etc/acs-config/auth_url} From 4f6daea861873440a40a9324911c9fdeb1c7db83 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 11:58:30 +0000 Subject: [PATCH 17/62] Fix misnamed grafana secret --- deploy/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/values.yaml b/deploy/values.yaml index 72917efcb..7659cb387 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -362,7 +362,7 @@ grafana: name: Keycloak allow_sign_up: true client_id: grafana - client_secret: $__file{/etc/secrets/auth_generic_oauth/client_secret} + client_secret: $__file{/etc/secrets/auth_generic_oauth/grafana} scopes: openid profile email auth_url: $__file{/etc/acs-config/auth_url} token_url: $__file{/etc/acs-config/token_url} From b35cf422ba682c076d27870b9772c6e7071eaa1b Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 13:49:08 +0000 Subject: [PATCH 18/62] Change to use realm instead of base URL in ACS config --- acs-service-setup/lib/openid-realm.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index d9570099b..ef420f34a 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -24,6 +24,7 @@ class RealmSetup { this.realm = "factory_plus"; this.base_url = service_setup.acs_config.domain; this.secure = service_setup.acs_config.secure; + this.acs_realm = service_setup.acs_config.realm; this.access_token = ""; this.refresh_token = ""; this.config = service_setup.config; @@ -53,14 +54,14 @@ class RealmSetup { subComponents: {}, config: { serverPrincipal: [ - `HTTP/openid.${this.base_url}@${this.base_url.toUpperCase()}`, + `HTTP/openid.${this.base_url}@${this.acs_realm}`, ], allowPasswordAuthentication: ["false"], debug: ["true"], keyTab: ["/etc/keytabs/server"], cachePolicy: ["DEFAULT"], updateProfileFirstLogin: ["false"], - kerberosRealm: [this.base_url.toUpperCase()], + kerberosRealm: [this.acs_realm], enabled: ["true"], }, }, From 9a80e558ed5885c071a1cc8675657c3be182c421 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 13:59:09 +0000 Subject: [PATCH 19/62] Use setTimeout from timers/promises --- acs-service-setup/lib/openid-realm.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index ef420f34a..00fb06a33 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -2,6 +2,7 @@ import crypto from "crypto"; import fs from "fs/promises"; import path from "path"; import { URLSearchParams } from "url"; +import { setTimeout } from "timers/promises"; /** * Create the startup OpenID realm `factory_plus`. @@ -174,7 +175,7 @@ class RealmSetup { await this.get_initial_access_token(this.refresh_token); await this.create_client(client_representation, true); } else if (status == 503) { - await this.wait(10000); + await setTimeout(milliseconds); await this.create_client(client_representation, false); } else { const error = await response.text(); @@ -228,7 +229,7 @@ class RealmSetup { return [data.access_token, data.refresh_token]; } else if (response.status == 503) { - await this.wait(10000); + await setTimeout(milliseconds); this.get_initial_access_token(); } else { const status = response.status; @@ -241,8 +242,4 @@ class RealmSetup { ); } } - - async wait(milliseconds) { - return new Promise((resolve) => setTimeout(resolve, milliseconds)); - } } From 284b8d966071c2ee7199b63d464ca1e4c75351c3 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 14:02:50 +0000 Subject: [PATCH 20/62] Timeout bugfix --- acs-service-setup/lib/openid-realm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 00fb06a33..b7f5a39e6 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -175,7 +175,7 @@ class RealmSetup { await this.get_initial_access_token(this.refresh_token); await this.create_client(client_representation, true); } else if (status == 503) { - await setTimeout(milliseconds); + await setTimeout(10000); await this.create_client(client_representation, false); } else { const error = await response.text(); @@ -229,7 +229,7 @@ class RealmSetup { return [data.access_token, data.refresh_token]; } else if (response.status == 503) { - await setTimeout(milliseconds); + await setTimeout(10000); this.get_initial_access_token(); } else { const status = response.status; From 15ed6a7419c46d520b9b72c8f1a528c895fd747b Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 14:08:38 +0000 Subject: [PATCH 21/62] Remove unused client keytab --- deploy/templates/openid/openid.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index eac2dd9ec..30fab6410 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -80,19 +80,6 @@ spec: --- -apiVersion: factoryplus.app.amrc.co.uk/v1 -kind: KerberosKey -metadata: - name: sv1openid - namespace: {{ .Release.Namespace }} -spec: - type: Random - principal: sv1openid@{{ .Values.identity.realm | required "values.identity.realm is required!" }} - secret: openid-keytabs/client - - ---- - apiVersion: factoryplus.app.amrc.co.uk/v1 kind: KerberosKey metadata: From e935fbbca50a83d3ef34ebb757fd8b618a4450b8 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 15:39:44 +0000 Subject: [PATCH 22/62] Add dynamic LocalSecret generation --- deploy/templates/openid/local-secrets.yaml | 12 +++++++----- deploy/templates/service-setup.yaml | 6 +++--- deploy/values.yaml | 7 +++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml index 94317ff4b..0d1bc0ac8 100644 --- a/deploy/templates/openid/local-secrets.yaml +++ b/deploy/templates/openid/local-secrets.yaml @@ -1,11 +1,13 @@ -{{if .Values.openid.enabled}} +{{ if .Values.openid.enabled }} +{{ range $clientName, $client := .Values.serviceSetup.config.openidClients }} apiVersion: factoryplus.app.amrc.co.uk/v1 kind: LocalSecret metadata: namespace: {{ .Release.Namespace }} - name: "keycloak-grafana-client" + name: "keycloak-{{ $clientName }}-client-secret" spec: format: Password - secret: "keycloak-grafana-client" - key: "grafana" -{{end }} + secret: "keycloak-clients" + key: "{{ $clientName }}" +{{ end }} +{{ end }} diff --git a/deploy/templates/service-setup.yaml b/deploy/templates/service-setup.yaml index f3d8982ea..8c0f2ebab 100644 --- a/deploy/templates/service-setup.yaml +++ b/deploy/templates/service-setup.yaml @@ -24,9 +24,9 @@ spec: name: krb5-conf - name: manager-ccache-storage emptyDir: { } - - name: grafana-client-secret + - name: client-secrets secret: - secretName: keycloak-grafana-client + secretName: keycloak-clients initContainers: - name: service-setup {{ include "amrc-connectivity-stack.image" (list . .Values.serviceSetup) | indent 10 }} @@ -62,7 +62,7 @@ spec: volumeMounts: - mountPath: /data name: git-checkouts - - name: grafana-client-secret + - name: client-secrets mountPath: /etc/secret readOnly: true - name: edge-helm-charts diff --git a/deploy/values.yaml b/deploy/values.yaml index 7659cb387..03c9a6cbe 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -375,11 +375,14 @@ grafana: readOnly: true optional: false extraSecretMounts: - - name: keycloak-grafana-client-mount - secretName: keycloak-grafana-client + - name: keycloak-client-mount + secretName: keycloak-clients defaultMode: 0440 mountPath: /etc/secrets/auth_generic_oauth readOnly: true + items: + - key: grafana + path: grafana sidecar: datasources: enabled: true From e0b480f6e13657b2a21dece9a3c5a05f310c2b36 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Wed, 26 Mar 2025 16:02:17 +0000 Subject: [PATCH 23/62] Fix helm loop bug --- deploy/templates/openid/local-secrets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml index 0d1bc0ac8..4386f488a 100644 --- a/deploy/templates/openid/local-secrets.yaml +++ b/deploy/templates/openid/local-secrets.yaml @@ -3,7 +3,7 @@ apiVersion: factoryplus.app.amrc.co.uk/v1 kind: LocalSecret metadata: - namespace: {{ .Release.Namespace }} + namespace: {{ $.Release.Namespace }} name: "keycloak-{{ $clientName }}-client-secret" spec: format: Password From 3493233e60027163cbfae548b26be8fe7e8e9db9 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 10:16:59 +0000 Subject: [PATCH 24/62] Remove `items` from keytabs mount This is left over from when we were pulling secrets from `krb5-keytabs`, and isn't helpful here. It's causing the pod to fail to start because the `client` key no longer exists. --- deploy/templates/openid/openid.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index 30fab6410..e041d1da1 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -59,11 +59,6 @@ spec: - name: openid-keytabs secret: secretName: openid-keytabs - items: - - path: client - key: client - - path: server - key: server --- apiVersion: v1 kind: Service From e51b3071e460aa228024bc8a24d0409c85f60fdc Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 10:33:58 +0000 Subject: [PATCH 25/62] Allow OpenID errors to cause service-setup to exit Without this we will silently fail when Keycloak is down. --- acs-service-setup/lib/openid-realm.js | 98 +++++++++++++-------------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index b7f5a39e6..5781b4dc8 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -95,32 +95,30 @@ class RealmSetup { this.log(`Attempting basic realm creation at: ${realm_url}`); - try { - const response = await fetch(realm_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.access_token}`, - }, - body: JSON.stringify(realm_representation), - }); - - if (!response.ok) { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_initial_access_token(this.refresh_token); - await this.create_basic_realm(realm_representation, true); - } else if (status == 503) { - await this.wait(10000); - await this.create_basic_realm(realm_representation, false); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } + const response = await fetch(realm_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.access_token}`, + }, + body: JSON.stringify(realm_representation), + }); + + if (!response.ok) { + const status = response.status; + + if (status == 401 && !is_retry) { + await this.get_initial_access_token(this.refresh_token); + await this.create_basic_realm(realm_representation, true); + } else if (status == 409) { + this.log("Realm already exists"); + } else if (status == 503) { + await setTimeout(10000); + await this.create_basic_realm(realm_representation, false); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); } - } catch (error) { - this.log(`Couldn't setup realm: ${error}`); } } @@ -174,6 +172,8 @@ class RealmSetup { if (status == 401 && !is_retry) { await this.get_initial_access_token(this.refresh_token); await this.create_client(client_representation, true); + } else if (status == 409) { + this.log("Client already exists"); } else if (status == 503) { await setTimeout(10000); await this.create_client(client_representation, false); @@ -213,33 +213,27 @@ class RealmSetup { params.append("password", this.password); } - try { - const response = await fetch(token_url, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: params, - }); - - if (response.ok) { - const data = await response.json(); - this.access_token = data.access_token; - this.refresh_token = data.refresh_token; - - return [data.access_token, data.refresh_token]; - } else if (response.status == 503) { - await setTimeout(10000); - this.get_initial_access_token(); - } else { - const status = response.status; - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } catch (error) { - this.log( - `Couldn't get an initial access token for realm setup: ${error}`, - ); + const response = await fetch(token_url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + + if (response.ok) { + const data = await response.json(); + this.access_token = data.access_token; + this.refresh_token = data.refresh_token; + + return [data.access_token, data.refresh_token]; + } else if (response.status == 503) { + await setTimeout(10000); + this.get_initial_access_token(); + } else { + const status = response.status; + const error = await response.text(); + throw new Error(`${status}: ${error}`); } } } From 47fab3e629cc5a3106d3e641a3f1a1e9bd0620ab Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 10:42:07 +0000 Subject: [PATCH 26/62] Reference client secrets correctly Since changing the LocalSecret generation the keys have changed. --- acs-service-setup/lib/openid-realm.js | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 5781b4dc8..11c953959 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -73,12 +73,12 @@ class RealmSetup { await this.get_initial_access_token(); await this.create_basic_realm(base_realm, false); - const client_configs = Object.values(this.config.openidClients); + const client_configs = Object.entries(this.config.openidClients); const enabled_clients = client_configs.filter( - (client) => client.enabled === true, + ([name, client]) => client.enabled === true, ); - for (const client of enabled_clients) { - await this.create_client(client, false); + for (const [name, client] of enabled_clients) { + await this.create_client(name, client, false); } } @@ -130,25 +130,25 @@ class RealmSetup { * @param {Boolean} is_retry - Whether the request is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. * @returns {Promise} Resolves when the client is created. */ - async create_client(client_representation, is_retry) { - const secret_path = path.join( - "/etc/secret", - client_representation.redirectHost, - ); + async create_client(name, client_representation, is_retry) { + const secret_path = path.join("/etc/secret", name); const content = await fs.readFile(secret_path, "utf8"); const client_secret = content.trim(); + const host = client_representation.redirectHost ?? name; + const url = `http${this.secure}://${host}.${this.base_url}`; + const client = { id: crypto.randomUUID(), clientId: client_representation.clientId, name: client_representation.name, - rootUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, - adminUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, - baseUrl: `http${this.secure}://${client_representation.redirectHost}.${this.base_url}`, + rootUrl: url, + adminUrl: url, + baseUrl: url, enabled: true, secret: client_secret, redirectUris: [ - `http${this.secure}://${client_representation.redirectHost}.${this.base_url}${client_representation.redirectPath}`, + `${url}${client_representation.redirectPath}`, ], }; @@ -173,7 +173,7 @@ class RealmSetup { await this.get_initial_access_token(this.refresh_token); await this.create_client(client_representation, true); } else if (status == 409) { - this.log("Client already exists"); + this.log("Client %o already exists", name); } else if (status == 503) { await setTimeout(10000); await this.create_client(client_representation, false); From 98af79f61559c02c4d7b947713f7e63627f2064d Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 11:52:18 +0000 Subject: [PATCH 27/62] Remove Grafana Basic auth middleware We should be using OAuth only now. --- deploy/templates/grafana/grafana-ingress.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/deploy/templates/grafana/grafana-ingress.yaml b/deploy/templates/grafana/grafana-ingress.yaml index 43801b005..e103bfc84 100644 --- a/deploy/templates/grafana/grafana-ingress.yaml +++ b/deploy/templates/grafana/grafana-ingress.yaml @@ -10,8 +10,6 @@ spec: routes: - match: Host(`grafana.{{.Values.acs.baseUrl | required "values.acs.baseUrl is required"}}`) kind: Rule - middlewares: - - name: basic-auth services: - name: acs-grafana port: 80 From 94e7ed0c4e5997cf06ac718130b570f6128ee6f7 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 12:13:54 +0000 Subject: [PATCH 28/62] Log realm details on creation --- acs-service-setup/lib/openid-realm.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 11c953959..8358aa7e9 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -104,7 +104,9 @@ class RealmSetup { body: JSON.stringify(realm_representation), }); - if (!response.ok) { + if (response.ok) { + this.log("Created new realm: %o", realm_representation); + } else { const status = response.status; if (status == 401 && !is_retry) { @@ -166,7 +168,9 @@ class RealmSetup { body: JSON.stringify(client), }); - if (!response.ok) { + if (response.ok) { + this.log("Created new realm client: %o", client_representation); + } else { const status = response.status; if (status == 401 && !is_retry) { From e83d8284c37d935ed4b6e06f16f7077a3f56d716 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 12:17:25 +0000 Subject: [PATCH 29/62] Give Keycloack a PVC We need to store the realm configuration somewhere. --- deploy/templates/openid/openid.yaml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index e041d1da1..88dd2e5be 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -52,6 +52,8 @@ spec: mountPath: /config/krb5-conf - name: openid-keytabs mountPath: /etc/keytabs + - name: data + mountPath: /opt/keycloak/data volumes: - name: krb5-conf configMap: @@ -59,6 +61,24 @@ spec: - name: openid-keytabs secret: secretName: openid-keytabs + - name: data + persistentVolumeClaim: + claimName: keycloak-data +--- +# XXX Could we use Postgres here instead? +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: keycloak-data + namespace: {{ .Release.Namespace }} + labels: + component: openid +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi --- apiVersion: v1 kind: Service @@ -72,9 +92,7 @@ spec: targetPort: 8080 selector: factory-plus.service: openid - --- - apiVersion: factoryplus.app.amrc.co.uk/v1 kind: KerberosKey metadata: From fcb2dc14ccfbfa67e71c95d4bd9a4eec05f581a3 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 12:47:59 +0000 Subject: [PATCH 30/62] Log full client information --- acs-service-setup/lib/openid-realm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 8358aa7e9..6f8d7fe37 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -169,7 +169,7 @@ class RealmSetup { }); if (response.ok) { - this.log("Created new realm client: %o", client_representation); + this.log("Created new realm client: %o", client); } else { const status = response.status; From d075eee61472d7feb68740c8d7a67c1291617e6b Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 12:48:11 +0000 Subject: [PATCH 31/62] Tell Grafana its external root URL Otherwise OAuth doesn't work. --- deploy/templates/grafana/grafani-ini.yaml | 3 ++- deploy/values.yaml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/templates/grafana/grafani-ini.yaml b/deploy/templates/grafana/grafani-ini.yaml index 60fbaa41f..672dd42e0 100644 --- a/deploy/templates/grafana/grafani-ini.yaml +++ b/deploy/templates/grafana/grafani-ini.yaml @@ -6,4 +6,5 @@ metadata: data: auth_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/auth token_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/token - api_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/userinfo \ No newline at end of file + api_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/userinfo + root_url: {{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}} diff --git a/deploy/values.yaml b/deploy/values.yaml index 25109bee0..2aa28e0a3 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -358,6 +358,7 @@ grafana: admin: existingSecret: grafana-admin-user grafana.ini: + root_url: "$__file{/etc/acs-config/root_url}" auth.basic: enabled: false auth.proxy: From 4f97679df0a3115a181cafa79c340f15405ceddc Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 12:50:10 +0000 Subject: [PATCH 32/62] Disable Grafana auth via the middleware Rename 'Keycloak' to 'Factory+'. Users don't care about the implementation. --- deploy/values.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/deploy/values.yaml b/deploy/values.yaml index 2aa28e0a3..d67697fda 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -361,14 +361,9 @@ grafana: root_url: "$__file{/etc/acs-config/root_url}" auth.basic: enabled: false - auth.proxy: - enabled: true - header_name: X-Auth-Principal - header_property: username - auto_sign_up: true auth.generic_oauth: enabled: true - name: Keycloak + name: "Factory+" allow_sign_up: true client_id: grafana client_secret: $__file{/etc/secrets/auth_generic_oauth/grafana} From d9a6c76ffb993b28a28e1bcc09eca43937f004c3 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 12:52:39 +0000 Subject: [PATCH 33/62] Grafana config changes * `root_url` is under `server`. * Disable the (useless) default login form. --- deploy/values.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy/values.yaml b/deploy/values.yaml index d67697fda..28be7cb2a 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -358,7 +358,10 @@ grafana: admin: existingSecret: grafana-admin-user grafana.ini: - root_url: "$__file{/etc/acs-config/root_url}" + server: + root_url: "$__file{/etc/acs-config/root_url}" + auth: + disable_login_form: true auth.basic: enabled: false auth.generic_oauth: From 688292bb68137130d181b5d9c5bf47aae52bb77b Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 13:04:43 +0000 Subject: [PATCH 34/62] Java doesn't accept KRB5_CONFIG There is a Java system property but I can't remember what it is. --- deploy/templates/mqtt/mqtt.yaml | 2 ++ deploy/templates/openid/openid.yaml | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deploy/templates/mqtt/mqtt.yaml b/deploy/templates/mqtt/mqtt.yaml index 70f25ed10..da6126b3a 100644 --- a/deploy/templates/mqtt/mqtt.yaml +++ b/deploy/templates/mqtt/mqtt.yaml @@ -57,6 +57,8 @@ spec: - name: VERBOSE value: {{.Values.mqtt.verbosity | quote | required "values.mqtt.verbosity is required!"}} volumeMounts: + # XXX This would be better without the subPath, but we need + # to use a Java system property to move krb5.conf. - mountPath: /etc/krb5.conf name: krb5-conf subPath: krb5.conf diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index 88dd2e5be..b0417fe25 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -38,8 +38,6 @@ spec: value: "edge" - name: KC_HEALTH_ENABLED value: "true" - - name: KRB5_CONFIG - value: /config/krb5-conf/krb5.conf ports: - name: http containerPort: 8080 @@ -48,8 +46,11 @@ spec: path: /health/ready port: 9000 volumeMounts: + # XXX This would be better without the subPath, but we need + # to use a Java system property to move krb5.conf. - name: krb5-conf - mountPath: /config/krb5-conf + mountPath: /etc/krb5.conf + subPath: krb5.conf - name: openid-keytabs mountPath: /etc/keytabs - name: data From fea872792163784de6cc562119e5a8ff8e37e84a Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Mar 2025 13:09:32 +0000 Subject: [PATCH 35/62] Keycloak needs to use Recreate strategy We are backed with a ReadWriteOnce PVC. --- deploy/templates/openid/openid.yaml | 5 ++++- deploy/values.yaml | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index b0417fe25..381b912f7 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -7,7 +7,10 @@ metadata: labels: component: openid spec: - replicas: {{ .Values.openid.replicas | default 1 }} + strategy: + type: Recreate + # We cannot allow more replicas with a RWO PVC backend + replicas: 1 selector: matchLabels: component: openid diff --git a/deploy/values.yaml b/deploy/values.yaml index 28be7cb2a..0ea5c991b 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -442,7 +442,6 @@ influxdb2: openid: enabled: true - replicas: 1 image: repository: quay.io/keycloak/keycloak tag: 26.1.1 From 8c209875654f2c3967ef05be5c20525582b7dd6f Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Mon, 31 Mar 2025 00:29:03 +0100 Subject: [PATCH 36/62] Fix login and logout for basic users Since we are using client_id for logout, Keycloak displays a logout confirmation page. This is skippable if we set id_token_hint. Grafana docs suggest Grafana will set this itself but it doesn't seem to work. --- acs-service-setup/lib/openid-realm.js | 12 +++++++----- deploy/templates/grafana/grafani-ini.yaml | 1 + deploy/values.yaml | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 6f8d7fe37..67470ee0e 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -57,11 +57,11 @@ class RealmSetup { serverPrincipal: [ `HTTP/openid.${this.base_url}@${this.acs_realm}`, ], - allowPasswordAuthentication: ["false"], + allowPasswordAuthentication: ["true"], debug: ["true"], keyTab: ["/etc/keytabs/server"], cachePolicy: ["DEFAULT"], - updateProfileFirstLogin: ["false"], + updateProfileFirstLogin: ["true"], kerberosRealm: [this.acs_realm], enabled: ["true"], }, @@ -149,9 +149,11 @@ class RealmSetup { baseUrl: url, enabled: true, secret: client_secret, - redirectUris: [ - `${url}${client_representation.redirectPath}`, - ], + redirectUris: [`${url}${client_representation.redirectPath}`], + attributes: { + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": url, + }, }; const client_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients`; diff --git a/deploy/templates/grafana/grafani-ini.yaml b/deploy/templates/grafana/grafani-ini.yaml index 672dd42e0..f1f65bdc0 100644 --- a/deploy/templates/grafana/grafani-ini.yaml +++ b/deploy/templates/grafana/grafani-ini.yaml @@ -8,3 +8,4 @@ data: token_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/token api_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/userinfo root_url: {{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}} + signout_redirect_url: {{ .Values.acs.secure | ternary "https://" "http://" }}openid.{{.Values.acs.baseUrl}}/realms/factory_plus/protocol/openid-connect/logout?post_logout_redirect_uri={{ .Values.acs.secure | ternary "https://" "http://" }}grafana.{{.Values.acs.baseUrl}}&client_id=grafana diff --git a/deploy/values.yaml b/deploy/values.yaml index 0ea5c991b..da27c497a 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -374,6 +374,7 @@ grafana: auth_url: $__file{/etc/acs-config/auth_url} token_url: $__file{/etc/acs-config/token_url} api_url: $__file{/etc/acs-config/api_url} + signout_redirect_url: $__file{/etc/acs-config/signout_redirect_url} extraConfigmapMounts: - name: ini-config mountPath: /etc/acs-config From 08a3193844e4225cf19423058d08ce9d7c5e4663 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 1 Apr 2025 00:16:01 +0100 Subject: [PATCH 37/62] Add client role mapping --- acs-service-setup/lib/openid-realm.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 67470ee0e..3d0629936 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -154,6 +154,26 @@ class RealmSetup { "backchannel.logout.session.required": "true", "post.logout.redirect.uris": url, }, + protocolMappers: [ + { + name: "client roles", + protocol: "openid-connect", + protocolMapper: "oidc-usermodel-client-role-mapper", + consentRequired: false, + config: { + "introspection.token.claim": "true", + multivalued: "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "${client_id}.roles", + "jsonType.label": "String", + "usermodel.clientRoleMapping.clientId": + client_representation.clientId, + }, + }, + ], }; const client_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients`; From f17aa0e395fbd650261c4949678ddbbb4e798c86 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 1 Apr 2025 00:54:17 +0100 Subject: [PATCH 38/62] Add client role creation --- acs-service-setup/lib/openid-realm.js | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 3d0629936..649d13573 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -192,6 +192,16 @@ class RealmSetup { if (response.ok) { this.log("Created new realm client: %o", client); + if ( + client_representation.roles && + Array.isArray(client_representation.roles) + ) { + await this.create_client_roles( + client.id, + client_representation.roles, + host, + ); + } } else { const status = response.status; @@ -213,6 +223,70 @@ class RealmSetup { } } + /** + * Create roles for a client by POSTing to the OpenID service. + * + * @async + * @param {string} clientId - The UUID of the client to create roles for. + * @param {Array} roles - Array of role configurations. + * @param {string} containerId - The container ID for the roles (name of the client). + * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. + * @returns {Promise} Resolves when all roles are created. + */ + async create_client_roles(clientId, roles, containerId, is_retry = false) { + const roles_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients/${clientId}/roles`; + + this.log(`Creating roles for client ${containerId} at: ${roles_url}`); + + for (const role of roles) { + const role_representation = { + id: crypto.randomUUID(), + name: role.name, + composite: false, + clientRole: true, + containerId: containerId, + }; + + try { + const response = await fetch(roles_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.access_token}`, + }, + body: JSON.stringify(role_representation), + }); + + if (response.ok) { + this.log(`Created role '${role.name}' for client '${clientId}'`); + } else { + const status = response.status; + + if (status == 401 && !is_retry) { + await this.get_initial_access_token(this.refresh_token); + await this.create_client_roles(clientId, roles, containerId, true); + break; + } else if (status == 409) { + this.log( + `Role '${role.name}' already exists for client '${containerId}'`, + ); + } else if (status == 503) { + await setTimeout(10000); + await this.create_client_roles(clientId, roles, containerId, false); + break; + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } catch (error) { + this.log( + `Couldn't create role '${role.name}' for client '${clientId}': ${error}`, + ); + } + } + } + /** * Fetch an initial access token for the user in the specified realm. This should only be used during realm setup. * From edb12fc04c1297e3fd1cef4d4418eb343fcd4724 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 1 Apr 2025 13:15:59 +0100 Subject: [PATCH 39/62] Add admin-cli permissions to create users --- acs-service-setup/lib/openid-realm.js | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 649d13573..b6fe849dc 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -40,6 +40,9 @@ class RealmSetup { * @returns {Promise} */ async run() { + let admin_cli_id = crypto.randomUUID(); + let admin_cli_secret = crypto.randomUUID(); + let base_realm = { id: crypto.randomUUID(), realm: this.realm, @@ -68,6 +71,65 @@ class RealmSetup { }, ], }, + clients: [ + { + id: admin_cli_id, + clientId: "admin-cli", + name: "${client_admin-cli}", + enabled: true, + clientAuthenticatorType: "client-secret", + secret: admin_cli_secret, + directAccessGrantsEnabled: true, + serviceAccountsEnabled: true, + authorizationServicesEnabled: true, + }, + ], + roles: { + client: { + "admin-cli": [ + { + id: crypto.randomUUID(), + name: "manage-users", + description: "", + composite: false, + clientRole: true, + containerId: admin_cli_id, + attributes: {}, + }, + { + id: crypto.randomUUID(), + name: "uma_protection", + composite: false, + clientRole: true, + containerId: admin_cli_id, + attributes: {}, + }, + { + id: crypto.randomUUID(), + name: "manage-realm", + description: "", + composite: false, + clientRole: true, + containerId: admin_cli_id, + attributes: {}, + }, + ], + }, + }, + users: [ + { + id: crypto.randomUUID(), + username: "service-account-admin-cli", + emailVerified: false, + enabled: true, + serviceAccountClientId: "admin-cli", + realmRoles: ["default-roles-factory_plus"], + clientRoles: { + "realm-management": ["manage-realm", "manage-users"], + "admin-cli": ["uma_protection"], + }, + }, + ], }; await this.get_initial_access_token(); From 7fa281d58504cd18f6d6a932ce05494c84cd5f00 Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 1 Apr 2025 13:23:18 +0100 Subject: [PATCH 40/62] Add default role to client --- acs-service-setup/lib/openid-realm.js | 1 + deploy/values.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index b6fe849dc..d6f40ab05 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -211,6 +211,7 @@ class RealmSetup { baseUrl: url, enabled: true, secret: client_secret, + defaultRoles: [client_representation.defaultRole], redirectUris: [`${url}${client_representation.redirectPath}`], attributes: { "backchannel.logout.session.required": "true", diff --git a/deploy/values.yaml b/deploy/values.yaml index da27c497a..ee3f53b8d 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -142,6 +142,7 @@ serviceSetup: - name: "viewer" - name: "editor" - name: "admin" + defaultRole: "viewer" edgeHelm: From 5189e03b523a4110c4f83af40612153bb644e37f Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 1 Apr 2025 14:23:31 +0100 Subject: [PATCH 41/62] Add creation of admin user --- acs-service-setup/lib/openid-realm.js | 117 ++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 8 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index d6f40ab05..0b6bc6dff 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -28,7 +28,10 @@ class RealmSetup { this.acs_realm = service_setup.acs_config.realm; this.access_token = ""; this.refresh_token = ""; + this.user_management_access_token = ""; this.config = service_setup.config; + this.admin_cli_id = crypto.randomUUID(); + this.admin_cli_secret = crypto.randomUUID(); } /** @@ -40,9 +43,6 @@ class RealmSetup { * @returns {Promise} */ async run() { - let admin_cli_id = crypto.randomUUID(); - let admin_cli_secret = crypto.randomUUID(); - let base_realm = { id: crypto.randomUUID(), realm: this.realm, @@ -73,12 +73,12 @@ class RealmSetup { }, clients: [ { - id: admin_cli_id, + id: this.admin_cli_id, clientId: "admin-cli", name: "${client_admin-cli}", enabled: true, clientAuthenticatorType: "client-secret", - secret: admin_cli_secret, + secret: this.admin_cli_secret, directAccessGrantsEnabled: true, serviceAccountsEnabled: true, authorizationServicesEnabled: true, @@ -93,7 +93,7 @@ class RealmSetup { description: "", composite: false, clientRole: true, - containerId: admin_cli_id, + containerId: this.admin_cli_id, attributes: {}, }, { @@ -101,7 +101,7 @@ class RealmSetup { name: "uma_protection", composite: false, clientRole: true, - containerId: admin_cli_id, + containerId: this.admin_cli_id, attributes: {}, }, { @@ -110,7 +110,7 @@ class RealmSetup { description: "", composite: false, clientRole: true, - containerId: admin_cli_id, + containerId: this.admin_cli_id, attributes: {}, }, ], @@ -142,6 +142,7 @@ class RealmSetup { for (const [name, client] of enabled_clients) { await this.create_client(name, client, false); } + await this.create_admin_user(false); } /** @@ -350,6 +351,66 @@ class RealmSetup { } } + /** + * Create the admin user in the OpenID service. + * @async + * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. + * @returns {Promise} Resolves when the user is created. + */ + async create_admin_user(is_retry) { + const admin_user = { + id: crypto.randomUUID(), + userName: this.username, + firstName: "Admin", + lastName: "User", + email: `admin@${this.acs_realm}`, + emailVerified: false, + enabled: true, + serviceAccountClientId: "admin-cli", + credentials: { + type: "password", + value: this.password, + temporary: "false", + }, + }; + + const user_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users`; + + this.log(`Attempting admin user creation at: ${user_url}`); + + try { + const response = await fetch(user_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.user_management_access_token}`, + }, + body: JSON.stringify(admin_user), + }); + + if (response.ok) { + this.log("Created new realm: %o", admin_user); + } else { + const status = response.status; + + if (status == 401 && !is_retry) { + await this.get_user_management_token(); + await this.create_admin_user(admin_user, true); + } else if (status == 409) { + this.log("Admin user %o already exists", admin_user); + } else if (status == 503) { + await setTimeout(10000); + await this.create_admin_user(admin_user, false); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } catch (error) { + this.log(`Couldn't setup admin user: ${error}`); + } + } + /** * Fetch an initial access token for the user in the specified realm. This should only be used during realm setup. * @@ -399,4 +460,44 @@ class RealmSetup { throw new Error(`${status}: ${error}`); } } + /** + * Fetch a token for the user management client in the Factory+ realm. + * + * Sets the `user_management_access_token` property of `OAuthRealm` as a side effect. + * + * @async + * @returns {Promise} Resolves to an access token. + */ + async get_user_management_token() { + const token_url = `http${this.secure}://openid.${this.base_url}/realms/${this.realm}/protocol/openid-connect/token`; + + this.log(`Attempting token request at: ${token_url}`); + + const params = new URLSearchParams(); + params.append("grant_type", "client_credentials"); + params.append("client_id", "admin-cli"); + params.append("client_secret", this.admin_cli_secret); + + const response = await fetch(token_url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + + if (response.ok) { + const data = await response.json(); + this.user_management_access_token = data.access_token; + + return data.access_token; + } else if (response.status == 503) { + await setTimeout(10000); + this.get_user_management_token(); + } else { + const status = response.status; + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } } From c4a33d2157e8661d8a1c4fb676ce3e2291cc751a Mon Sep 17 00:00:00 2001 From: Kavan Price Date: Tue, 1 Apr 2025 17:03:47 +0100 Subject: [PATCH 42/62] Add client role mapping for admin user --- acs-service-setup/lib/openid-realm.js | 163 +++++++++++++++++++++++++- deploy/values.yaml | 2 + 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 0b6bc6dff..07aeabea6 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -134,6 +134,7 @@ class RealmSetup { await this.get_initial_access_token(); await this.create_basic_realm(base_realm, false); + await this.create_admin_user(false); const client_configs = Object.entries(this.config.openidClients); const enabled_clients = client_configs.filter( @@ -141,8 +142,8 @@ class RealmSetup { ); for (const [name, client] of enabled_clients) { await this.create_client(name, client, false); + await this.create_client_role_mapping(client.clientId, client.adminRole); } - await this.create_admin_user(false); } /** @@ -204,7 +205,7 @@ class RealmSetup { const url = `http${this.secure}://${host}.${this.base_url}`; const client = { - id: crypto.randomUUID(), + id: client_representation.clientId, clientId: client_representation.clientId, name: client_representation.name, rootUrl: url, @@ -411,6 +412,163 @@ class RealmSetup { } } + /** + * Create client-level role mapping for the admin user. + + * @async + * @param {string} client_id - The ID of the client containing the role. + * @param {string} role_name - The name of the role. + * @returns {Promise} Resolves when the mapping is created. + */ + async create_client_role_mapping(client_id, role_name) { + const admin_user_id = await this.get_admin_user_id(false); + const role_id = await this.get_client_role_id( + client_id, + admin_user_id, + role_name, + ); + + const role_list = [ + { + id: role_id, + name: role_name, + }, + ]; + + const role_mapping_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; + + this.log("Attempting role mapping using: %o", role_list); + + try { + this.get_user_management_token(); + const response = await fetch(role_mapping_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.user_management_access_token}`, + }, + body: JSON.stringify(role_list), + }); + + if (response.ok) { + this.log( + `Admin role mapping completed successfully for client ${client_id}`, + ); + } else { + const status = response.status; + + if (status == 401 && !is_retry) { + await this.get_user_management_token(); + await this.create_client_role_mapping(client_id, role_name, true); + } else if (status == 409) { + this.log(`Role mapping for ${client_id} already done.`); + } else if (status == 503) { + await setTimeout(10000); + await this.create_client_role_mapping(client_id, role_name, false); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } catch (error) { + this.log(`Couldn't setup client role mapping for ${client_id}: ${error}`); + } + } + + /** + * Get the ID of the admin user. This isn't the same as the ID given during creation. + * + * @async + * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. + * @returns {Promise} Resolves to the admin user ID. + */ + async get_admin_user_id(is_retry) { + const user_query_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users?exact=true&username=${this.username}`; + + await this.get_user_management_token(); + const response = await fetch(user_query_url, { + method: "GET", + headers: { + Authorization: `Bearer ${this.user_management_access_token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + return data[0].id; + } else { + const status = response.status; + + if (status == 401 && !is_retry) { + await this.get_user_management_token(); + return await this.get_admin_user_id(true); + } else if (status == 503) { + await setTimeout(10000); + return await this.get_admin_user_id(false); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } + + /** + * Get the ID of the applicable role given the containing client ID, the user ID, and the role name. + * + * @async + * @param {string} client_id - ID of the client containing the role. + * @param {string} user_id - ID of the user to which the role will apply. + * @param {string} role_name - Name of the role. + * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. + * @returns {Promise} Resolves to the ID of the role. + */ + async get_client_role_id(client_id, user_id, role_name, is_retry) { + const role_query_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users/${user_id}/role-mappings/clients/${client_id}/available`; + + await this.get_user_management_token(); + const response = await fetch(role_query, { + method: "GET", + headers: { + Authorization: `Bearer ${this.user_management_access_token}`, + }, + }); + + if (response.ok) { + const roleList = await response.json(); + for (const role of roleList) { + if (role.name === role_name) { + return role.id; + } + } + throw new Error( + `No role found with role name ${role_name} in ${client_id} available for ${user_id}`, + ); + } else { + const status = response.status; + + if (status == 401 && !is_retry) { + await this.get_user_management_token(); + return await this.get_client_role_id( + client_id, + user_id, + role_name, + true, + ); + } else if (status == 503) { + await setTimeout(10000); + return await this.get_client_role_id( + client_id, + user_id, + role_name, + false, + ); + } else { + const error = await response.text(); + throw new Error(`${status}: ${error}`); + } + } + } + /** * Fetch an initial access token for the user in the specified realm. This should only be used during realm setup. * @@ -460,6 +618,7 @@ class RealmSetup { throw new Error(`${status}: ${error}`); } } + /** * Fetch a token for the user management client in the Factory+ realm. * diff --git a/deploy/values.yaml b/deploy/values.yaml index ee3f53b8d..18c45c007 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -142,7 +142,9 @@ serviceSetup: - name: "viewer" - name: "editor" - name: "admin" + - name: "grafanaAdmin" defaultRole: "viewer" + adminRole: "grafanaAdmin" edgeHelm: From 9d44e3dc83a0db94a033427be566f00867641bae Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 11:08:56 +0100 Subject: [PATCH 43/62] Refactor OpenID setup The code was becoming very tangled and difficult to follow with duplicated retry logic everywhere. --- acs-service-setup/lib/openid-realm.js | 512 ++++++++++---------------- 1 file changed, 203 insertions(+), 309 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 07aeabea6..5fcef2361 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -15,6 +15,39 @@ export async function create_openid_realm(service_setup) { await new RealmSetup(service_setup).run(); } +class FetchError extends Error { + constructor (status, msg) { + super(`Fetch error: ${status}: ${msg}`); + this.status = status; + } + + /** Construct and throw a FetchError. + * @async + * @arg response A Fetch Response object + */ + static async throwOf (response) { + const msg = await response.text(); + throw new this(response.status, msg); + } + + /** Swallow expected errors. + * This is intended to be used with Promise.catch. The function + * returned will rethrow unexpected errors and return undefined for + * expected errors. + * @arg codes Expected error status codes + * @returns A catch function + */ + static expect (...codes) { + return err => { + if (!(err instanceof this)) + throw err; + if (!codes.includes(err.status)) + throw err; + return; + }; + } +} + class RealmSetup { constructor(service_setup) { const { fplus } = service_setup; @@ -26,14 +59,91 @@ class RealmSetup { this.base_url = service_setup.acs_config.domain; this.secure = service_setup.acs_config.secure; this.acs_realm = service_setup.acs_config.realm; - this.access_token = ""; - this.refresh_token = ""; - this.user_management_access_token = ""; this.config = service_setup.config; this.admin_cli_id = crypto.randomUUID(); this.admin_cli_secret = crypto.randomUUID(); } + /** Generate a URL to the OpenID server. + * @param {string} path - The path for the URL + * @returns {string} A full URL + */ + url (path) { + return `http${this.secure}://openid.${this.base_url}/${path}`; + } + + /** Perform a fetch with error handling. + * 5xx errors will delay and retry. + * Other HTTP errors will throw a FetchError. + * @async + * @arg request - Request object + * @returns {Response} A Fetch Response + */ + async try_fetch (request) { + const response = await fetch(request); + + if (response.ok) + return response; + + if (response.status >= 500 && response.status < 600) { + await setTimeout(10000); + this.log("Retrying %s", request.url); + return this.try_fetch(request); + } + + await FetchError.throwOf(response); + } + + /** Make a request with a token. + * 5xx errors will delay and retry. + * If we get a 401 we retry, once, with a new token. + * Other HTTP errors will throw. + * The token method is called on `this`. + * The Request object will be modified with the token header. + * @arg tokensrc - Async function to get a token. + * @arg request - Request object + * @returns A Response + */ + async with_token (tokensrc, request, retry) { + const token = await tokensrc.call(this, retry); + + request.headers.set("Authorization", `Bearer ${token}`); + const doit = fetch(request); + const response = await (retry ? doit : doit.catch(FetchError.expect(401))); + + if (!response) + return this.with_token(tokensrc, request, true); + + return response; + } + + /** Create an OpenID resouce. + * Performs a request with the appropriate token. + * Success returns true. 409 returns false. Other errors throw. + * @arg opts Options for creation + * @returns Did we create the resource? + */ + async openid_create (opts) { + const url = this.url(opts.path); + this.log("Attempting to create %s at %s", opts.name, url); + + const request = new Request(url, { + method: opts.method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(opts.body), + }); + + const created = this.with_token(opts.tokensrc, request) + .catch(FetchError.expect(409)); + + if (created) + this.log("Created %s: %o", opts.name, opts.body); + else + this.log("Can't create %s: already exists", opts.name); + + return !!created; + } + /** * Run setup for the realm. This generates the full realm representation. * @@ -132,16 +242,15 @@ class RealmSetup { ], }; - await this.get_initial_access_token(); - await this.create_basic_realm(base_realm, false); - await this.create_admin_user(false); + await this.create_basic_realm(base_realm); + await this.create_admin_user(); const client_configs = Object.entries(this.config.openidClients); const enabled_clients = client_configs.filter( ([name, client]) => client.enabled === true, ); for (const [name, client] of enabled_clients) { - await this.create_client(name, client, false); + await this.create_client(name, client); await this.create_client_role_mapping(client.clientId, client.adminRole); } } @@ -151,41 +260,16 @@ class RealmSetup { * * @async * @param {Object} realm_representation - An object containing all the values that should be created for the realm. - * @param {Boolean} is_retry - Whether the request is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. * @returns {Promise} Resolves when the realm is created. */ - async create_basic_realm(realm_representation, is_retry) { - const realm_url = `http${this.secure}://openid.${this.base_url}/admin/realms`; - - this.log(`Attempting basic realm creation at: ${realm_url}`); - - const response = await fetch(realm_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.access_token}`, - }, - body: JSON.stringify(realm_representation), + async create_basic_realm(realm_representation) { + await this.openid_create({ + name: `realm %o`, + tokensrc: this.get_initial_access_token, + method: "POST", + path: `admin/realms`, + body: realm_representation, }); - - if (response.ok) { - this.log("Created new realm: %o", realm_representation); - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_initial_access_token(this.refresh_token); - await this.create_basic_realm(realm_representation, true); - } else if (status == 409) { - this.log("Realm already exists"); - } else if (status == 503) { - await setTimeout(10000); - await this.create_basic_realm(realm_representation, false); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } } /** @@ -241,51 +325,16 @@ class RealmSetup { ], }; - const client_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients`; - - this.log(`Attempting client creation at: ${client_url}`); - - try { - const response = await fetch(client_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.access_token}`, - }, - body: JSON.stringify(client), - }); + const created = await this.openid_create({ + name: `realm client %o`, + tokensrc: this.get_initial_access_token, + path: `admin/realms/${this.realm}/clients`, + method: "POST", + body: client, + }); - if (response.ok) { - this.log("Created new realm client: %o", client); - if ( - client_representation.roles && - Array.isArray(client_representation.roles) - ) { - await this.create_client_roles( - client.id, - client_representation.roles, - host, - ); - } - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_initial_access_token(this.refresh_token); - await this.create_client(client_representation, true); - } else if (status == 409) { - this.log("Client %o already exists", name); - } else if (status == 503) { - await setTimeout(10000); - await this.create_client(client_representation, false); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } - } catch (error) { - this.log(`Couldn't setup client: ${error}`); - } + if (created && client_representation.roles) + await this.create_client_roles(client.id, client_representation.roles, host); } /** @@ -295,14 +344,9 @@ class RealmSetup { * @param {string} clientId - The UUID of the client to create roles for. * @param {Array} roles - Array of role configurations. * @param {string} containerId - The container ID for the roles (name of the client). - * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. * @returns {Promise} Resolves when all roles are created. */ - async create_client_roles(clientId, roles, containerId, is_retry = false) { - const roles_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/clients/${clientId}/roles`; - - this.log(`Creating roles for client ${containerId} at: ${roles_url}`); - + async create_client_roles(clientId, roles, containerId) { for (const role of roles) { const role_representation = { id: crypto.randomUUID(), @@ -312,43 +356,13 @@ class RealmSetup { containerId: containerId, }; - try { - const response = await fetch(roles_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.access_token}`, - }, - body: JSON.stringify(role_representation), - }); - - if (response.ok) { - this.log(`Created role '${role.name}' for client '${clientId}'`); - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_initial_access_token(this.refresh_token); - await this.create_client_roles(clientId, roles, containerId, true); - break; - } else if (status == 409) { - this.log( - `Role '${role.name}' already exists for client '${containerId}'`, - ); - } else if (status == 503) { - await setTimeout(10000); - await this.create_client_roles(clientId, roles, containerId, false); - break; - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } - } catch (error) { - this.log( - `Couldn't create role '${role.name}' for client '${clientId}': ${error}`, - ); - } + await this.openid_create({ + name: `Role ${role.name} for client ${clientId}`, + tokensrc: this.get_initial_access_token, + path: `admin/realms/${this.realm}/clients/${clientId}/roles`, + method: "POST", + body: role_representation, + }); } } @@ -375,41 +389,13 @@ class RealmSetup { }, }; - const user_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users`; - - this.log(`Attempting admin user creation at: ${user_url}`); - - try { - const response = await fetch(user_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.user_management_access_token}`, - }, - body: JSON.stringify(admin_user), - }); - - if (response.ok) { - this.log("Created new realm: %o", admin_user); - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_user_management_token(); - await this.create_admin_user(admin_user, true); - } else if (status == 409) { - this.log("Admin user %o already exists", admin_user); - } else if (status == 503) { - await setTimeout(10000); - await this.create_admin_user(admin_user, false); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } - } catch (error) { - this.log(`Couldn't setup admin user: ${error}`); - } + await this.openid_create({ + name: `Admin user`, + tokensrc: this.get_user_management_token, + method: "POST", + path: `admin/realms/${this.realm}/users`, + body: admin_user, + }); } /** @@ -421,12 +407,8 @@ class RealmSetup { * @returns {Promise} Resolves when the mapping is created. */ async create_client_role_mapping(client_id, role_name) { - const admin_user_id = await this.get_admin_user_id(false); - const role_id = await this.get_client_role_id( - client_id, - admin_user_id, - role_name, - ); + const admin_user_id = await this.get_admin_user_id(); + const role_id = await this.get_client_role_id(client_id, admin_user_id, role_name); const role_list = [ { @@ -435,81 +417,31 @@ class RealmSetup { }, ]; - const role_mapping_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; - - this.log("Attempting role mapping using: %o", role_list); - - try { - this.get_user_management_token(); - const response = await fetch(role_mapping_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.user_management_access_token}`, - }, - body: JSON.stringify(role_list), - }); - - if (response.ok) { - this.log( - `Admin role mapping completed successfully for client ${client_id}`, - ); - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_user_management_token(); - await this.create_client_role_mapping(client_id, role_name, true); - } else if (status == 409) { - this.log(`Role mapping for ${client_id} already done.`); - } else if (status == 503) { - await setTimeout(10000); - await this.create_client_role_mapping(client_id, role_name, false); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } - } catch (error) { - this.log(`Couldn't setup client role mapping for ${client_id}: ${error}`); - } + const path = `admin/realms/${this.realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; + await this.openid_create({ + name: `Role mapping for ${client_id}`, + tokensrc: this.get_user_management_token, + method: "POST", + path, + body: role_list, + }); } /** * Get the ID of the admin user. This isn't the same as the ID given during creation. * * @async - * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. * @returns {Promise} Resolves to the admin user ID. */ - async get_admin_user_id(is_retry) { - const user_query_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users?exact=true&username=${this.username}`; + async get_admin_user_id() { + const user_query_url = this.url(`admin/realms/${this.realm}/users?exact=true&username=${this.username}`); - await this.get_user_management_token(); - const response = await fetch(user_query_url, { - method: "GET", - headers: { - Authorization: `Bearer ${this.user_management_access_token}`, - }, - }); + const response = await this.with_token( + this.get_user_management_token, + new Request(user_query_url)); - if (response.ok) { - const data = await response.json(); - return data[0].id; - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_user_management_token(); - return await this.get_admin_user_id(true); - } else if (status == 503) { - await setTimeout(10000); - return await this.get_admin_user_id(false); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } - } + const data = await response.json(); + return data[0].id; } /** @@ -523,50 +455,21 @@ class RealmSetup { * @returns {Promise} Resolves to the ID of the role. */ async get_client_role_id(client_id, user_id, role_name, is_retry) { - const role_query_url = `http${this.secure}://openid.${this.base_url}/admin/realms/${this.realm}/users/${user_id}/role-mappings/clients/${client_id}/available`; + const role_query_url = this.url(`admin/realms/${this.realm}/users/${user_id}/role-mappings/clients/${client_id}/available`); - await this.get_user_management_token(); - const response = await fetch(role_query, { - method: "GET", - headers: { - Authorization: `Bearer ${this.user_management_access_token}`, - }, - }); + const response = await this.with_token( + this.get_user_management_token, + new Request(role_query_url)); - if (response.ok) { - const roleList = await response.json(); - for (const role of roleList) { - if (role.name === role_name) { - return role.id; - } - } - throw new Error( - `No role found with role name ${role_name} in ${client_id} available for ${user_id}`, - ); - } else { - const status = response.status; - - if (status == 401 && !is_retry) { - await this.get_user_management_token(); - return await this.get_client_role_id( - client_id, - user_id, - role_name, - true, - ); - } else if (status == 503) { - await setTimeout(10000); - return await this.get_client_role_id( - client_id, - user_id, - role_name, - false, - ); - } else { - const error = await response.text(); - throw new Error(`${status}: ${error}`); + const roleList = await response.json(); + for (const role of roleList) { + if (role.name === role_name) { + return role.id; } } + throw new Error( + `No role found with role name ${role_name} in ${client_id} available for ${user_id}`, + ); } /** @@ -575,16 +478,21 @@ class RealmSetup { * Sets the `access_token` and `refresh_token` properties of `OAuthRealm` as a side effect. * * @async - * @param {string | undefined} [refresh_token] - Optional refresh token. If this is passed we use it with bearer auth. - * @returns {Promise<[string, string]} Resolves to an access token and a refresh token. + * @param {boolean} force - Do we need a fresh token? + * @returns {Promise} Resolves to an access token */ - async get_initial_access_token(refresh_token) { - const token_url = `http${this.secure}://openid.${this.base_url}/realms/master/protocol/openid-connect/token`; + async get_initial_access_token(force) { + const { access_token, refresh_token } = this; + + if (access_token && !force) + return access_token; + + const token_url = this.url(`realms/master/protocol/openid-connect/token`); this.log(`Attempting token request at: ${token_url}`); const params = new URLSearchParams(); - if (refresh_token != undefined) { + if (this.refresh_token != undefined) { params.append("grant_type", "refresh_token"); params.append("client_id", "admin-cli"); params.append("refresh_token", refresh_token); @@ -595,40 +503,35 @@ class RealmSetup { params.append("password", this.password); } - const response = await fetch(token_url, { + const response = await this.try_fetch(new Request(token_url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: params, - }); + })); - if (response.ok) { - const data = await response.json(); - this.access_token = data.access_token; - this.refresh_token = data.refresh_token; + const data = await response.json(); + this.access_token = data.access_token; + this.refresh_token = data.refresh_token; - return [data.access_token, data.refresh_token]; - } else if (response.status == 503) { - await setTimeout(10000); - this.get_initial_access_token(); - } else { - const status = response.status; - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } + return data.access_token; } /** * Fetch a token for the user management client in the Factory+ realm. * - * Sets the `user_management_access_token` property of `OAuthRealm` as a side effect. + * Sets the `user_management_token` property of `OAuthRealm` as a side effect. * * @async + * @param {boolean} force - Do we need a fresh token? * @returns {Promise} Resolves to an access token. */ - async get_user_management_token() { - const token_url = `http${this.secure}://openid.${this.base_url}/realms/${this.realm}/protocol/openid-connect/token`; + async get_user_management_token(force) { + if (this.user_management_token && !force) + return this.user_management_token; + + const token_url = this.url(`/realms/${this.realm}/protocol/openid-connect/token`); this.log(`Attempting token request at: ${token_url}`); @@ -637,26 +540,17 @@ class RealmSetup { params.append("client_id", "admin-cli"); params.append("client_secret", this.admin_cli_secret); - const response = await fetch(token_url, { + const response = await this.try_fetch(new Request(token_url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: params, - }); + })); - if (response.ok) { - const data = await response.json(); - this.user_management_access_token = data.access_token; + const data = await response.json(); + this.user_management_token = data.access_token; - return data.access_token; - } else if (response.status == 503) { - await setTimeout(10000); - this.get_user_management_token(); - } else { - const status = response.status; - const error = await response.text(); - throw new Error(`${status}: ${error}`); - } + return data.access_token; } } From cf237dcfa30855767bc8919538bdacae51b0efb8 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 11:09:44 +0100 Subject: [PATCH 44/62] Always create client roles The list might have changed from last time. We don't attempt to delete old roles at this point, this is for the user to handle if necessary. --- acs-service-setup/lib/openid-realm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 5fcef2361..ff3077fdd 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -333,7 +333,7 @@ class RealmSetup { body: client, }); - if (created && client_representation.roles) + if (client_representation.roles) await this.create_client_roles(client.id, client_representation.roles, host); } From b7e7294644fb51b2f6524b554c8053f41cd9956c Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 11:26:00 +0100 Subject: [PATCH 45/62] Use a LocalSecret for the admin service client This is a client secret like any other; it needs to be preserved across ACS upgrades, and so needs to come from a LocalSecret. --- acs-service-setup/lib/openid-realm.js | 74 ++++++++++++---------- deploy/templates/openid/local-secrets.yaml | 12 ++++ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index ff3077fdd..a6078b982 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -60,8 +60,6 @@ class RealmSetup { this.secure = service_setup.acs_config.secure; this.acs_realm = service_setup.acs_config.realm; this.config = service_setup.config; - this.admin_cli_id = crypto.randomUUID(); - this.admin_cli_secret = crypto.randomUUID(); } /** Generate a URL to the OpenID server. @@ -144,6 +142,12 @@ class RealmSetup { return !!created; } + async client_secret (name) { + const secret_path = path.join("/etc/secret", name); + const content = await fs.readFile(secret_path, "utf8"); + return content.trim(); + } + /** * Run setup for the realm. This generates the full realm representation. * @@ -153,7 +157,32 @@ class RealmSetup { * @returns {Promise} */ async run() { - let base_realm = { + this.admin_cli_secret = await this.client_secret("_admin"); + + await this.create_basic_realm(); + await this.create_admin_user(); + + const client_configs = Object.entries(this.config.openidClients); + const enabled_clients = client_configs.filter( + ([name, client]) => client.enabled === true, + ); + for (const [name, client] of enabled_clients) { + await this.create_client(name, client); + await this.create_client_role_mapping(client.clientId, client.adminRole); + } + } + + /** + * Create a new realm by POSTing to the OpenID service. Throws if the response is not ok. + * + * @async + * @param {Object} realm_representation - An object containing all the values that should be created for the realm. + * @returns {Promise} Resolves when the realm is created. + */ + async create_basic_realm() { + const admin_cli_id = crypto.randomUUID(); + + const base_realm = { id: crypto.randomUUID(), realm: this.realm, displayName: "Factory+", @@ -183,9 +212,9 @@ class RealmSetup { }, clients: [ { - id: this.admin_cli_id, + id: admin_cli_id, clientId: "admin-cli", - name: "${client_admin-cli}", + name: "Admin service account", enabled: true, clientAuthenticatorType: "client-secret", secret: this.admin_cli_secret, @@ -203,7 +232,7 @@ class RealmSetup { description: "", composite: false, clientRole: true, - containerId: this.admin_cli_id, + containerId: admin_cli_id, attributes: {}, }, { @@ -211,7 +240,7 @@ class RealmSetup { name: "uma_protection", composite: false, clientRole: true, - containerId: this.admin_cli_id, + containerId: admin_cli_id, attributes: {}, }, { @@ -220,7 +249,7 @@ class RealmSetup { description: "", composite: false, clientRole: true, - containerId: this.admin_cli_id, + containerId: admin_cli_id, attributes: {}, }, ], @@ -242,33 +271,12 @@ class RealmSetup { ], }; - await this.create_basic_realm(base_realm); - await this.create_admin_user(); - - const client_configs = Object.entries(this.config.openidClients); - const enabled_clients = client_configs.filter( - ([name, client]) => client.enabled === true, - ); - for (const [name, client] of enabled_clients) { - await this.create_client(name, client); - await this.create_client_role_mapping(client.clientId, client.adminRole); - } - } - - /** - * Create a new realm by POSTing to the OpenID service. Throws if the response is not ok. - * - * @async - * @param {Object} realm_representation - An object containing all the values that should be created for the realm. - * @returns {Promise} Resolves when the realm is created. - */ - async create_basic_realm(realm_representation) { await this.openid_create({ name: `realm %o`, tokensrc: this.get_initial_access_token, method: "POST", path: `admin/realms`, - body: realm_representation, + body: base_realm, }); } @@ -281,9 +289,7 @@ class RealmSetup { * @returns {Promise} Resolves when the client is created. */ async create_client(name, client_representation, is_retry) { - const secret_path = path.join("/etc/secret", name); - const content = await fs.readFile(secret_path, "utf8"); - const client_secret = content.trim(); + const client_secret = await this.client_secret(name); const host = client_representation.redirectHost ?? name; const url = `http${this.secure}://${host}.${this.base_url}`; @@ -325,7 +331,7 @@ class RealmSetup { ], }; - const created = await this.openid_create({ + await this.openid_create({ name: `realm client %o`, tokensrc: this.get_initial_access_token, path: `admin/realms/${this.realm}/clients`, diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml index 4386f488a..ee1051bd8 100644 --- a/deploy/templates/openid/local-secrets.yaml +++ b/deploy/templates/openid/local-secrets.yaml @@ -1,5 +1,6 @@ {{ if .Values.openid.enabled }} {{ range $clientName, $client := .Values.serviceSetup.config.openidClients }} +--- apiVersion: factoryplus.app.amrc.co.uk/v1 kind: LocalSecret metadata: @@ -10,4 +11,15 @@ spec: secret: "keycloak-clients" key: "{{ $clientName }}" {{ end }} +--- +apiVersion: factoryplus.app.amrc.co.uk/v1 +kind: LocalSecret +metadata: + namespace: {{ $.Release.Namespace }} + name: "keycloak-admin-secret" +spec: + format: Password + secret: "keycloak-clients" + key: "_admin" + {{ end }} From 28b296d51725e7540586c8f38557ee3fded10466 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 11:51:59 +0100 Subject: [PATCH 46/62] Fix some logging --- acs-service-setup/lib/openid-realm.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index a6078b982..8c23f731b 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -272,7 +272,7 @@ class RealmSetup { }; await this.openid_create({ - name: `realm %o`, + name: `realm`, tokensrc: this.get_initial_access_token, method: "POST", path: `admin/realms`, @@ -332,7 +332,7 @@ class RealmSetup { }; await this.openid_create({ - name: `realm client %o`, + name: `realm client ${name}`, tokensrc: this.get_initial_access_token, path: `admin/realms/${this.realm}/clients`, method: "POST", @@ -363,7 +363,7 @@ class RealmSetup { }; await this.openid_create({ - name: `Role ${role.name} for client ${clientId}`, + name: `role ${role.name} for client ${clientId}`, tokensrc: this.get_initial_access_token, path: `admin/realms/${this.realm}/clients/${clientId}/roles`, method: "POST", @@ -396,7 +396,7 @@ class RealmSetup { }; await this.openid_create({ - name: `Admin user`, + name: `admin user`, tokensrc: this.get_user_management_token, method: "POST", path: `admin/realms/${this.realm}/users`, @@ -425,7 +425,7 @@ class RealmSetup { const path = `admin/realms/${this.realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; await this.openid_create({ - name: `Role mapping for ${client_id}`, + name: `role mapping for ${client_id}`, tokensrc: this.get_user_management_token, method: "POST", path, @@ -495,7 +495,7 @@ class RealmSetup { const token_url = this.url(`realms/master/protocol/openid-connect/token`); - this.log(`Attempting token request at: ${token_url}`); + this.log("%s token request at: %s", force ? "Refresh" : "Initial", token_url); const params = new URLSearchParams(); if (this.refresh_token != undefined) { @@ -537,9 +537,9 @@ class RealmSetup { if (this.user_management_token && !force) return this.user_management_token; - const token_url = this.url(`/realms/${this.realm}/protocol/openid-connect/token`); + const token_url = this.url(`realms/${this.realm}/protocol/openid-connect/token`); - this.log(`Attempting token request at: ${token_url}`); + this.log("%s token request at: %s", force ? "Refresh" : "Initial", token_url); const params = new URLSearchParams(); params.append("grant_type", "client_credentials"); From c177e658eee184ea040068e412a528baabf96ad3 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 12:28:02 +0100 Subject: [PATCH 47/62] Missing await --- acs-service-setup/lib/openid-realm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 8c23f731b..5a341ae27 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -131,7 +131,7 @@ class RealmSetup { body: JSON.stringify(opts.body), }); - const created = this.with_token(opts.tokensrc, request) + const created = await this.with_token(opts.tokensrc, request) .catch(FetchError.expect(409)); if (created) From ae0290b305688b43e8c49063d0c8bff8d1f11563 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 12:28:27 +0100 Subject: [PATCH 48/62] We need to call try_fetch, not native fetch --- acs-service-setup/lib/openid-realm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 5a341ae27..9a0f4c346 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -106,7 +106,7 @@ class RealmSetup { const token = await tokensrc.call(this, retry); request.headers.set("Authorization", `Bearer ${token}`); - const doit = fetch(request); + const doit = this.try_fetch(request); const response = await (retry ? doit : doit.catch(FetchError.expect(401))); if (!response) From c77c634d6715818078629e3b29fe46f59aa6e865 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 12:29:20 +0100 Subject: [PATCH 49/62] Error in admin user creation --- acs-service-setup/lib/openid-realm.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 9a0f4c346..42c3d2553 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -375,13 +375,12 @@ class RealmSetup { /** * Create the admin user in the OpenID service. * @async - * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. * @returns {Promise} Resolves when the user is created. */ - async create_admin_user(is_retry) { + async create_admin_user() { const admin_user = { id: crypto.randomUUID(), - userName: this.username, + username: this.username, firstName: "Admin", lastName: "User", email: `admin@${this.acs_realm}`, @@ -498,7 +497,7 @@ class RealmSetup { this.log("%s token request at: %s", force ? "Refresh" : "Initial", token_url); const params = new URLSearchParams(); - if (this.refresh_token != undefined) { + if (refresh_token != undefined) { params.append("grant_type", "refresh_token"); params.append("client_id", "admin-cli"); params.append("refresh_token", refresh_token); From 62c36cd18126ee12d119c170d472b43ac8e888b7 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 13:10:34 +0100 Subject: [PATCH 50/62] The admin@ account is no longer the service account I'm getting weird 400 errors from this method and I wasn't sure if this was the cause. But apparently not... --- acs-service-setup/lib/openid-realm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 42c3d2553..0adbfe798 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -386,7 +386,6 @@ class RealmSetup { email: `admin@${this.acs_realm}`, emailVerified: false, enabled: true, - serviceAccountClientId: "admin-cli", credentials: { type: "password", value: this.password, From 01a0585f952945c5d107caf7daf9af28b120d9fd Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 13:11:01 +0100 Subject: [PATCH 51/62] Log if the admin user doesn't exist --- acs-service-setup/lib/openid-realm.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 0adbfe798..bba8d7f62 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -445,7 +445,10 @@ class RealmSetup { new Request(user_query_url)); const data = await response.json(); - return data[0].id; + const id = data[0]?.id; + if (id == null) + throw new Error("Admin user doesn't exist"); + return id; } /** From 628e3447d5d73ff42d0a1c207823672cccdfecc3 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 13:18:15 +0100 Subject: [PATCH 52/62] UserRepresentation.credentials is an array This was giving a really unhelpful (and incorrect) 400 error. --- acs-service-setup/lib/openid-realm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index bba8d7f62..e2a83de20 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -386,11 +386,11 @@ class RealmSetup { email: `admin@${this.acs_realm}`, emailVerified: false, enabled: true, - credentials: { + credentials: [{ type: "password", value: this.password, temporary: "false", - }, + }], }; await this.openid_create({ From da8e5d98b27426ea9382dd4dda1b8640dcdcf060 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 14:22:35 +0100 Subject: [PATCH 53/62] We must clone a Request to reuse it Because a Request can contain a ReadableStream body we need to clone it to reuse it. --- acs-service-setup/lib/openid-realm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index e2a83de20..4619f59d5 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -86,7 +86,7 @@ class RealmSetup { if (response.status >= 500 && response.status < 600) { await setTimeout(10000); this.log("Retrying %s", request.url); - return this.try_fetch(request); + return this.try_fetch(new Request(request)); } await FetchError.throwOf(response); From fbfbdfce6e3b359ffcbf8e1369c7d2ce09a0ed7a Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 3 Apr 2025 14:37:55 +0100 Subject: [PATCH 54/62] Rename the Keycloak bootstrap user I am having a lot of trouble creating the admin@ user account; Keycloak keeps returning 409 even though it later denies the account exists. I am wondering if this is because of a conflict with the initial 'temporary' bootstrap account username. Rename to avoid this; at the same time change the password, this isn't linked to admin@ any more. --- acs-service-setup/lib/openid-realm.js | 4 ++-- deploy/templates/openid/local-secrets.yaml | 14 ++++++++++++-- deploy/templates/openid/openid.yaml | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 4619f59d5..4c95f8588 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -53,8 +53,7 @@ class RealmSetup { const { fplus } = service_setup; this.log = fplus.debug.bound("oauth"); - this.username = fplus.opts.username; - this.password = fplus.opts.password; + this.username = "_bootstrap"; this.realm = "factory_plus"; this.base_url = service_setup.acs_config.domain; this.secure = service_setup.acs_config.secure; @@ -157,6 +156,7 @@ class RealmSetup { * @returns {Promise} */ async run() { + this.password = await this.client_secret("_bootstrap"); this.admin_cli_secret = await this.client_secret("_admin"); await this.create_basic_realm(); diff --git a/deploy/templates/openid/local-secrets.yaml b/deploy/templates/openid/local-secrets.yaml index ee1051bd8..884f69af8 100644 --- a/deploy/templates/openid/local-secrets.yaml +++ b/deploy/templates/openid/local-secrets.yaml @@ -5,7 +5,7 @@ apiVersion: factoryplus.app.amrc.co.uk/v1 kind: LocalSecret metadata: namespace: {{ $.Release.Namespace }} - name: "keycloak-{{ $clientName }}-client-secret" + name: "keycloak-client-{{ $clientName }}" spec: format: Password secret: "keycloak-clients" @@ -16,7 +16,17 @@ apiVersion: factoryplus.app.amrc.co.uk/v1 kind: LocalSecret metadata: namespace: {{ $.Release.Namespace }} - name: "keycloak-admin-secret" + name: "keycloak-admin-bootstrap" +spec: + format: Password + secret: "keycloak-clients" + key: "_bootstrap" +--- +apiVersion: factoryplus.app.amrc.co.uk/v1 +kind: LocalSecret +metadata: + namespace: {{ $.Release.Namespace }} + name: "keycloak-admin-admin" spec: format: Password secret: "keycloak-clients" diff --git a/deploy/templates/openid/openid.yaml b/deploy/templates/openid/openid.yaml index 381b912f7..29c57e1a9 100644 --- a/deploy/templates/openid/openid.yaml +++ b/deploy/templates/openid/openid.yaml @@ -31,12 +31,12 @@ spec: args: ["start-dev"] env: - name: KEYCLOAK_ADMIN - value: "admin" + value: "_bootstrap" - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: secretKeyRef: - name: admin-password - key: password + name: keycloak-clients + key: _bootstrap - name: KC_PROXY value: "edge" - name: KC_HEALTH_ENABLED From 1461db30d6c63795da0f4205c132c551e6c915ec Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 09:59:17 +0100 Subject: [PATCH 55/62] Create admin@ linked to Kerberos The Keycloak user-create endpoint is consistently returning 409 when we attempt to create an account for the admin user. I think this may be because we have federation set up to Kerberos and Keycloak refuses to support non-Kerberos users in this realm. IMHO this is desirable in any case. We don't have a real human driving to perform the first-login flow, so go round the houses a bit. First we request a token 'on behalf of' admin@; this will fail because we haven't set up the profile, but will create the user on the Keycloak side. Then we can go in with our user management credentials and finish the setup. --- acs-service-setup/lib/openid-realm.js | 131 ++++++++++++++------------ deploy/templates/service-setup.yaml | 4 + 2 files changed, 75 insertions(+), 60 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 4c95f8588..012d51b28 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -53,6 +53,7 @@ class RealmSetup { const { fplus } = service_setup; this.log = fplus.debug.bound("oauth"); + this.fplus = fplus; this.username = "_bootstrap"; this.realm = "factory_plus"; this.base_url = service_setup.acs_config.domain; @@ -90,6 +91,27 @@ class RealmSetup { await FetchError.throwOf(response); } + + /** Fetch a token. + * @async + * @arg realm - The realm name. + * @arg params - An object of parameters for the token request. + */ + async fetch_token (realm, params) { + const token_url = this.url(`realms/${realm}/protocol/openid-connect/token`); + + this.log("Token request [%s]: %s", params.grant_type, token_url); + + const response = await this.try_fetch(new Request(token_url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(params), + })); + + return response.json(); +} /** Make a request with a token. * 5xx errors will delay and retry. @@ -160,7 +182,7 @@ class RealmSetup { this.admin_cli_secret = await this.client_secret("_admin"); await this.create_basic_realm(); - await this.create_admin_user(); + this.admin_user_id = await this.create_admin_user(); const client_configs = Object.entries(this.config.openidClients); const enabled_clients = client_configs.filter( @@ -378,28 +400,42 @@ class RealmSetup { * @returns {Promise} Resolves when the user is created. */ async create_admin_user() { + const { username, password } = this.fplus.opts; + + this.log("Setting up %s account", username); + /* Attempt a login as the admin user. This will fail if the user + * profile hasn't been created yet but will create the user on the + * OpenID side as a side-effect. We need to use the client secret + * here as OAuth doesn't appear to provide any way for users to log + * in on their own account. */ + await this.fetch_token(this.realm, { + grant_type: "password", + client_id: "admin-cli", + client_secret: this.admin_cli_secret, + username, + password, + }).catch(FetchError.expect(400)); + + /* Fetch the user id (allocated by Keycloak) */ + const id = await this.get_user_id(username); + const admin_user = { - id: crypto.randomUUID(), - username: this.username, firstName: "Admin", lastName: "User", - email: `admin@${this.acs_realm}`, + email: `${username}@${this.acs_realm}`, emailVerified: false, - enabled: true, - credentials: [{ - type: "password", - value: this.password, - temporary: "false", - }], }; + /* Update the profile */ await this.openid_create({ - name: `admin user`, + name: `admin profile`, tokensrc: this.get_user_management_token, - method: "POST", - path: `admin/realms/${this.realm}/users`, + method: "PUT", + path: `admin/realms/${this.realm}/users/${id}`, body: admin_user, }); + + return id; } /** @@ -411,7 +447,7 @@ class RealmSetup { * @returns {Promise} Resolves when the mapping is created. */ async create_client_role_mapping(client_id, role_name) { - const admin_user_id = await this.get_admin_user_id(); + const { admin_user_id, realm } = this; const role_id = await this.get_client_role_id(client_id, admin_user_id, role_name); const role_list = [ @@ -421,7 +457,7 @@ class RealmSetup { }, ]; - const path = `admin/realms/${this.realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; + const path = `admin/realms/${realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; await this.openid_create({ name: `role mapping for ${client_id}`, tokensrc: this.get_user_management_token, @@ -437,8 +473,8 @@ class RealmSetup { * @async * @returns {Promise} Resolves to the admin user ID. */ - async get_admin_user_id() { - const user_query_url = this.url(`admin/realms/${this.realm}/users?exact=true&username=${this.username}`); + async get_user_id(username) { + const user_query_url = this.url(`admin/realms/${this.realm}/users?exact=true&username=${username}`); const response = await this.with_token( this.get_user_management_token, @@ -447,7 +483,7 @@ class RealmSetup { const data = await response.json(); const id = data[0]?.id; if (id == null) - throw new Error("Admin user doesn't exist"); + throw new Error(`User ${username} doesn't exist`); return id; } @@ -494,34 +530,22 @@ class RealmSetup { if (access_token && !force) return access_token; - const token_url = this.url(`realms/master/protocol/openid-connect/token`); - - this.log("%s token request at: %s", force ? "Refresh" : "Initial", token_url); - - const params = new URLSearchParams(); - if (refresh_token != undefined) { - params.append("grant_type", "refresh_token"); - params.append("client_id", "admin-cli"); - params.append("refresh_token", refresh_token); - } else { - params.append("grant_type", "password"); - params.append("client_id", "admin-cli"); - params.append("username", this.username); - params.append("password", this.password); - } + const params = { + client_id: "admin-cli", + ...(refresh_token ? { + grant_type: "refresh_token", + refresh_token, + } : { + grant_type: "password", + username: this.username, + password: this.password, + }), + }; - const response = await this.try_fetch(new Request(token_url, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: params, - })); + const data = await this.fetch_token("master", params); - const data = await response.json(); this.access_token = data.access_token; this.refresh_token = data.refresh_token; - return data.access_token; } @@ -538,26 +562,13 @@ class RealmSetup { if (this.user_management_token && !force) return this.user_management_token; - const token_url = this.url(`realms/${this.realm}/protocol/openid-connect/token`); - - this.log("%s token request at: %s", force ? "Refresh" : "Initial", token_url); - - const params = new URLSearchParams(); - params.append("grant_type", "client_credentials"); - params.append("client_id", "admin-cli"); - params.append("client_secret", this.admin_cli_secret); - - const response = await this.try_fetch(new Request(token_url, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: params, - })); + const data = await this.fetch_token(this.realm, { + grant_type: "client_credentials", + client_id: "admin-cli", + client_secret: this.admin_cli_secret, + }); - const data = await response.json(); this.user_management_token = data.access_token; - return data.access_token; } } diff --git a/deploy/templates/service-setup.yaml b/deploy/templates/service-setup.yaml index 261867dee..84658e00b 100644 --- a/deploy/templates/service-setup.yaml +++ b/deploy/templates/service-setup.yaml @@ -35,6 +35,10 @@ spec: value: http://directory.{{ .Release.Namespace }}.svc.cluster.local - name: SERVICE_USERNAME value: admin + # Currently the openid realm setup relies on having the + # admin@ credentials available, with a password rather than + # a keytab. If service-setup is ever moved over to using its + # own account there will need to be changes to that code. - name: SERVICE_PASSWORD valueFrom: secretKeyRef: From 2034cae2fcf39dab3ef1032b9b32f61d611190a7 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 11:02:19 +0100 Subject: [PATCH 56/62] Assign admin user roles correctly The `available` endpoint only returns roles which the user is not already assigned to. We need to check existing assignments first. --- acs-service-setup/lib/openid-realm.js | 56 +++++++++------------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 012d51b28..a9cb55e60 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -448,22 +448,32 @@ class RealmSetup { */ async create_client_role_mapping(client_id, role_name) { const { admin_user_id, realm } = this; - const role_id = await this.get_client_role_id(client_id, admin_user_id, role_name); - - const role_list = [ - { - id: role_id, - name: role_name, - }, - ]; const path = `admin/realms/${realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; + + const get_roles = type => this.with_token( + this.get_user_management_token, + new Request(this.url(`${path}/${type}`)) + ).then(res => res.json()); + + const assigned = await get_roles(""); + if (assigned.some(r => r.name == role_name)) { + this.log("Admin user already a member of role %s", role_name); + return; + } + + const available = await get_roles("available"); + const role = available.find(r => r.name == role_name); + + if (!role) + throw new Error(`Cannot find role ${role_name} for ${client_id}`); + await this.openid_create({ name: `role mapping for ${client_id}`, tokensrc: this.get_user_management_token, method: "POST", path, - body: role_list, + body: [role], }); } @@ -487,34 +497,6 @@ class RealmSetup { return id; } - /** - * Get the ID of the applicable role given the containing client ID, the user ID, and the role name. - * - * @async - * @param {string} client_id - ID of the client containing the role. - * @param {string} user_id - ID of the user to which the role will apply. - * @param {string} role_name - Name of the role. - * @param {Boolean} is_retry - Whether this is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. - * @returns {Promise} Resolves to the ID of the role. - */ - async get_client_role_id(client_id, user_id, role_name, is_retry) { - const role_query_url = this.url(`admin/realms/${this.realm}/users/${user_id}/role-mappings/clients/${client_id}/available`); - - const response = await this.with_token( - this.get_user_management_token, - new Request(role_query_url)); - - const roleList = await response.json(); - for (const role of roleList) { - if (role.name === role_name) { - return role.id; - } - } - throw new Error( - `No role found with role name ${role_name} in ${client_id} available for ${user_id}`, - ); - } - /** * Fetch an initial access token for the user in the specified realm. This should only be used during realm setup. * From 1d6aea2fb9620f81d8882691e49b085d81ce9450 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 11:12:12 +0100 Subject: [PATCH 57/62] Remove Grafana admin user secret This should now be configured via OpenID. --- .../templates/grafana/grafana-admin-user.yaml | 17 ----------------- .../{grafani-ini.yaml => grafana-ini.yaml} | 0 deploy/values.yaml | 2 -- 3 files changed, 19 deletions(-) delete mode 100644 deploy/templates/grafana/grafana-admin-user.yaml rename deploy/templates/grafana/{grafani-ini.yaml => grafana-ini.yaml} (100%) diff --git a/deploy/templates/grafana/grafana-admin-user.yaml b/deploy/templates/grafana/grafana-admin-user.yaml deleted file mode 100644 index 0597cbccb..000000000 --- a/deploy/templates/grafana/grafana-admin-user.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{ if .Values.grafana.enabled }} -{{- if not (lookup "v1" "Secret" .Release.Namespace "grafana-admin-user") }} - -apiVersion: v1 -kind: Secret -metadata: - name: "grafana-admin-user" - namespace: {{ .Release.Namespace }} - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: - admin-user: {{ (printf "admin@%s" (.Values.identity.realm | required "values.identity.realm is required!") | b64enc) | quote }} - admin-password: {{ (printf "" | b64enc) | quote }} - -{{- end }} -{{- end -}} diff --git a/deploy/templates/grafana/grafani-ini.yaml b/deploy/templates/grafana/grafana-ini.yaml similarity index 100% rename from deploy/templates/grafana/grafani-ini.yaml rename to deploy/templates/grafana/grafana-ini.yaml diff --git a/deploy/values.yaml b/deploy/values.yaml index 18c45c007..beff963d9 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -358,8 +358,6 @@ grafana: persistence: enabled: true envFromSecret: influxdb-auth - admin: - existingSecret: grafana-admin-user grafana.ini: server: root_url: "$__file{/etc/acs-config/root_url}" From b063321cf4695b550f741afb978fcc6dc3b0a5dc Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 11:22:18 +0100 Subject: [PATCH 58/62] We can't remove the grafana-internal admin user If we don't specify an admin account secret Grafana generates one with the username `admin`, which conflicts with the Kerberos account we are trying to use via OpenID. Replace the Secret but change the username and give it a password. It won't be possible to log in as this user in any case as the login dialog is disabled. --- deploy/templates/grafana/grafana-admin-user.yaml | 11 +++++++++++ deploy/values.yaml | 2 ++ 2 files changed, 13 insertions(+) create mode 100644 deploy/templates/grafana/grafana-admin-user.yaml diff --git a/deploy/templates/grafana/grafana-admin-user.yaml b/deploy/templates/grafana/grafana-admin-user.yaml new file mode 100644 index 000000000..ee08f14d7 --- /dev/null +++ b/deploy/templates/grafana/grafana-admin-user.yaml @@ -0,0 +1,11 @@ +{{ if .Values.grafana.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: "grafana-admin-user" + namespace: {{ .Release.Namespace }} +type: Opaque +stringData: + admin-user: "_bootstrap" + admin-password: {{ randAlphaNum 20 | quote }} +{{- end -}} diff --git a/deploy/values.yaml b/deploy/values.yaml index beff963d9..18c45c007 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -358,6 +358,8 @@ grafana: persistence: enabled: true envFromSecret: influxdb-auth + admin: + existingSecret: grafana-admin-user grafana.ini: server: root_url: "$__file{/etc/acs-config/root_url}" From e263eac15e43410f6e32d637e2f7f2ba29699209 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 13:52:22 +0100 Subject: [PATCH 59/62] Assign Grafana roles from OAuth --- deploy/values.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deploy/values.yaml b/deploy/values.yaml index 18c45c007..5a37d9d67 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -378,6 +378,12 @@ grafana: token_url: $__file{/etc/acs-config/token_url} api_url: $__file{/etc/acs-config/api_url} signout_redirect_url: $__file{/etc/acs-config/signout_redirect_url} + role_attribute_path: | + contains(roles[*], 'grafanaAdmin') && 'GrafanaAdmin' + || contains(roles[*], 'admin') && 'Admin' + || contains(roles[*], 'editor') && 'Editor' + || 'Viewer' + allow_assign_grafana_admin: true extraConfigmapMounts: - name: ini-config mountPath: /etc/acs-config From 847a97b7b8327da12e77cf1c7e2ad76579cb6601 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 14:56:30 +0100 Subject: [PATCH 60/62] Try to get OpenID roles visible in Grafana It seems like the only way to have the default install set up admin@ as a Grafana admin is for Grafana to defer to Keycloak completely for role information. This means that admin@ must also be able to manage the roles assigned to other OpenID users; this information can no longer be managed in Grafana. * Allow assigning multiple default and admin roles. * Allow assigning roles within builtin clients. * Assign admin@ the roles needed to be a realm admin in Keycloak. * Consistently use the key in openidClients as client ID. --- acs-service-setup/lib/openid-realm.js | 64 +++++++++++---------------- deploy/values.yaml | 25 ++++++++--- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index a9cb55e60..1a3a67ce5 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -188,9 +188,10 @@ class RealmSetup { const enabled_clients = client_configs.filter( ([name, client]) => client.enabled === true, ); - for (const [name, client] of enabled_clients) { - await this.create_client(name, client); - await this.create_client_role_mapping(client.clientId, client.adminRole); + for (const [clientId, client] of enabled_clients) { + if (!client.builtin) + await this.create_client(clientId, client); + await this.create_client_role_mappings(clientId, client.adminRoles); } } @@ -307,54 +308,33 @@ class RealmSetup { * * @async * @param {Object} client_representation - An object containing all the values that should be created for the client. - * @param {Boolean} is_retry - Whether the request is a retry after a 401. If this is false and a 401 is returned, this will retry after refreshing the token. * @returns {Promise} Resolves when the client is created. */ - async create_client(name, client_representation, is_retry) { - const client_secret = await this.client_secret(name); + async create_client(clientId, client_representation) { + const client_secret = await this.client_secret(clientId); - const host = client_representation.redirectHost ?? name; + const host = client_representation.redirectHost ?? clientId; const url = `http${this.secure}://${host}.${this.base_url}`; const client = { - id: client_representation.clientId, - clientId: client_representation.clientId, + id: clientId, + clientId, name: client_representation.name, rootUrl: url, adminUrl: url, baseUrl: url, enabled: true, secret: client_secret, - defaultRoles: [client_representation.defaultRole], + defaultRoles: client_representation.defaultRoles, redirectUris: [`${url}${client_representation.redirectPath}`], attributes: { "backchannel.logout.session.required": "true", "post.logout.redirect.uris": url, }, - protocolMappers: [ - { - name: "client roles", - protocol: "openid-connect", - protocolMapper: "oidc-usermodel-client-role-mapper", - consentRequired: false, - config: { - "introspection.token.claim": "true", - multivalued: "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "${client_id}.roles", - "jsonType.label": "String", - "usermodel.clientRoleMapping.clientId": - client_representation.clientId, - }, - }, - ], }; await this.openid_create({ - name: `realm client ${name}`, + name: `realm client ${clientId}`, tokensrc: this.get_initial_access_token, path: `admin/realms/${this.realm}/clients`, method: "POST", @@ -446,7 +426,7 @@ class RealmSetup { * @param {string} role_name - The name of the role. * @returns {Promise} Resolves when the mapping is created. */ - async create_client_role_mapping(client_id, role_name) { + async create_client_role_mappings (client_id, roles) { const { admin_user_id, realm } = this; const path = `admin/realms/${realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; @@ -456,24 +436,30 @@ class RealmSetup { new Request(this.url(`${path}/${type}`)) ).then(res => res.json()); + const want = new Set(roles); + const assigned = await get_roles(""); - if (assigned.some(r => r.name == role_name)) { - this.log("Admin user already a member of role %s", role_name); - return; + const have = assigned.map(r => r.name).filter(n => want.has(n)); + for (const name of have) { + this.log("Admin already has role %s for %s", name, client_id); + want.delete(name); } const available = await get_roles("available"); - const role = available.find(r => r.name == role_name); + const assign = available.filter(r => want.has(r.name)); - if (!role) - throw new Error(`Cannot find role ${role_name} for ${client_id}`); + if (assign.length != want.size) { + this.log("Required roles: %s", [...want].join(", ")); + this.log("Available roles: %s", assign.map(r => r.name).join(", ")); + throw new Error("Not all required roles are available"); + } await this.openid_create({ name: `role mapping for ${client_id}`, tokensrc: this.get_user_management_token, method: "POST", path, - body: [role], + body: assign, }); } diff --git a/deploy/values.yaml b/deploy/values.yaml index 5a37d9d67..1b212c9ad 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -134,18 +134,31 @@ serviceSetup: openidClients: grafana: enabled: true - clientId: "grafana" name: "Grafana" - redirectHost: "grafana" redirectPath: "/login/generic_oauth" roles: - name: "viewer" - name: "editor" - name: "admin" - name: "grafanaAdmin" - defaultRole: "viewer" - adminRole: "grafanaAdmin" - + defaultRoles: [viewer] + adminRoles: [grafanaAdmin] + realm-management: + enabled: true + builtin: true + adminRoles: + - view-realm + - view-users + - view-clients + - view-events + - manage-realm + - manage-users + - create-client + - manage-clients + - manage-events + - view-identity-providers + - manage-identity-providers + - impersonation edgeHelm: enabled: true @@ -378,7 +391,7 @@ grafana: token_url: $__file{/etc/acs-config/token_url} api_url: $__file{/etc/acs-config/api_url} signout_redirect_url: $__file{/etc/acs-config/signout_redirect_url} - role_attribute_path: | + role_attribute_path: > contains(roles[*], 'grafanaAdmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' From a3791164e9c6425a1f1a004a575b7fde7f4c0e34 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 15:21:47 +0100 Subject: [PATCH 61/62] Restore OpenID role mapper I'm guessing at this point; none of this appears to be documented. The Grafana documentation doesn't mention needing this but it's not working without, so try changing `claim.name` to the `roles` which I think Grafana is expecting. --- acs-service-setup/lib/openid-realm.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 1a3a67ce5..6473c14c8 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -331,6 +331,26 @@ class RealmSetup { "backchannel.logout.session.required": "true", "post.logout.redirect.uris": url, }, + protocolMappers: [ + { + name: "client roles", + protocol: "openid-connect", + protocolMapper: "oidc-usermodel-client-role-mapper", + consentRequired: false, + config: { + "introspection.token.claim": "true", + multivalued: "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "usermodel.clientRoleMapping.clientId": + client_representation.clientId, + }, + }, + ], }; await this.openid_create({ @@ -429,6 +449,8 @@ class RealmSetup { async create_client_role_mappings (client_id, roles) { const { admin_user_id, realm } = this; + this.log("Checking role mappings for %s", client_id); + const path = `admin/realms/${realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; const get_roles = type => this.with_token( @@ -445,6 +467,8 @@ class RealmSetup { want.delete(name); } + if (!want.size) return; + const available = await get_roles("available"); const assign = available.filter(r => want.has(r.name)); From b4f4da20460614fdaf115011fc89ad95854b89ec Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 4 Apr 2025 15:44:53 +0100 Subject: [PATCH 62/62] We need to look up the client ID. Confusingly, this is not the `clientId` field but the `id` field. This is a generated UUID for `realm-management`. --- acs-service-setup/lib/openid-realm.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/acs-service-setup/lib/openid-realm.js b/acs-service-setup/lib/openid-realm.js index 6473c14c8..e990763ff 100644 --- a/acs-service-setup/lib/openid-realm.js +++ b/acs-service-setup/lib/openid-realm.js @@ -446,21 +446,30 @@ class RealmSetup { * @param {string} role_name - The name of the role. * @returns {Promise} Resolves when the mapping is created. */ - async create_client_role_mappings (client_id, roles) { + async create_client_role_mappings (client_name, roles) { const { admin_user_id, realm } = this; - this.log("Checking role mappings for %s", client_id); + this.log("Checking role mappings for %s", client_name); - const path = `admin/realms/${realm}/users/${admin_user_id}/role-mappings/clients/${client_id}`; - - const get_roles = type => this.with_token( + const fetch_info = path => this.with_token( this.get_user_management_token, - new Request(this.url(`${path}/${type}`)) + new Request(this.url(path)) ).then(res => res.json()); + const realm_p = `admin/realms/${realm}`; + const clients = await fetch_info(`${realm_p}/clients?clientId=${client_name}`); + + if (clients.length != 1) + throw new Error(`Can't find client ${client_name}`); + + const client_id = clients[0].id; + this.log("Found client-id %s", client_id); + + const map_p = `${realm_p}/users/${admin_user_id}/role-mappings/clients/${client_id}`; + const want = new Set(roles); - const assigned = await get_roles(""); + const assigned = await fetch_info(map_p); const have = assigned.map(r => r.name).filter(n => want.has(n)); for (const name of have) { this.log("Admin already has role %s for %s", name, client_id); @@ -469,7 +478,7 @@ class RealmSetup { if (!want.size) return; - const available = await get_roles("available"); + const available = await fetch_info(`${map_p}/available`); const assign = available.filter(r => want.has(r.name)); if (assign.length != want.size) {