feat(monitoring): per-instance OIDC selector for the Grafana instance (Phase 1) - #3176
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds OIDC authentication support for Monitoring Grafana, including a new ChangesGrafana OIDC Feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant HelmValues
participant MonitoringChart
participant Keycloak
participant Grafana
participant Secret
HelmValues->>MonitoringChart: set spec.oidc.mode and customConfig
MonitoringChart->>Secret: render or reuse client-secret
MonitoringChart->>Keycloak: provision client, scope, and groups
MonitoringChart->>Grafana: render auth.generic_oauth or custom INI wiring
Grafana->>Keycloak: OIDC login
Keycloak-->>Grafana: token with role and audience claims
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the Monitoring package by adding support for OIDC authentication for Grafana instances. It provides a structured way for operators to integrate with the platform's central Keycloak realm or bring their own identity provider. The changes include new Helm templates for managing OIDC-related resources, schema updates for the Monitoring CR, and automated end-to-end tests to ensure reliable configuration rendering. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request implements Phase 1 of OIDC integration for the Grafana instance, introducing three identity modes: None, System (using the platform's Keycloak), and CustomConfig (for tenant-supplied OIDC issuers). The changes include updates to the Helm chart templates, values schema, documentation, and new E2E and unit tests. The review feedback constructively points out opportunities to apply defensive programming across several templates (grafana.yaml, _helpers.tpl, oidc-client-secret.yaml, and oidc-keycloak.yaml) by safely navigating .Values.oidc to prevent potential nil pointer evaluation errors if the OIDC configuration is omitted or null.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| {{- $oidcMode := .Values.oidc.mode | default "None" }} | ||
| {{- if eq $oidcMode "CustomConfig" }} | ||
| {{- include "monitoring.oidc.assertCustomConfigXor" . }} | ||
| {{- end }} | ||
| {{- $customInline := (.Values.oidc.customConfig.config | default dict) }} | ||
| {{- $customSecretRefName := dig "secretRef" "name" "" (.Values.oidc.customConfig | default dict) }} | ||
| {{- $useCustomInline := and (eq $oidcMode "CustomConfig") (gt (len $customInline) 0) }} | ||
| {{- $useCustomSecretRef := and (eq $oidcMode "CustomConfig") (ne $customSecretRefName "") }} |
There was a problem hiding this comment.
To prevent potential nil pointer evaluation errors if .Values.oidc is omitted or set to null in the user's custom values, use defensive programming to safely navigate the nested maps.
{{- $oidc := .Values.oidc | default dict }}
{{- $oidcMode := $oidc.mode | default "None" }}
{{- if eq $oidcMode "CustomConfig" }}
{{- include "monitoring.oidc.assertCustomConfigXor" . }}
{{- end }}
{{- $customConfig := $oidc.customConfig | default dict }}
{{- $customInline := $customConfig.config | default dict }}
{{- $customSecretRefName := dig "secretRef" "name" "" $customConfig }}
{{- $useCustomInline := and (eq $oidcMode "CustomConfig") (gt (len $customInline) 0) }}
{{- $useCustomSecretRef := and (eq $oidcMode "CustomConfig") (ne $customSecretRefName "") }}| {{- define "monitoring.oidc.assertCustomConfigXor" -}} | ||
| {{- $inline := (.Values.oidc.customConfig.config | default dict) -}} | ||
| {{- $secretName := dig "secretRef" "name" "" (.Values.oidc.customConfig | default dict) -}} | ||
| {{- $hasInline := gt (len $inline) 0 -}} | ||
| {{- $hasSecretRef := ne ($secretName | toString) "" -}} | ||
| {{- if and $hasInline $hasSecretRef -}} | ||
| {{- fail "spec.oidc.customConfig: set exactly one of `config` (inline) or `secretRef.name` — they are mutually exclusive." -}} | ||
| {{- end -}} | ||
| {{- if not (or $hasInline $hasSecretRef) -}} | ||
| {{- fail "spec.oidc.mode: CustomConfig requires either spec.oidc.customConfig.config (inline generic_oauth map) or spec.oidc.customConfig.secretRef.name (Secret with an `auth.ini` key)." -}} | ||
| {{- end -}} | ||
| {{- end -}} |
There was a problem hiding this comment.
Safely navigate .Values.oidc to prevent nil pointer evaluation errors if oidc is omitted or set to null in the values.
{{- define "monitoring.oidc.assertCustomConfigXor" -}}
{{- $oidc := .Values.oidc | default dict -}}
{{- $customConfig := $oidc.customConfig | default dict -}}
{{- $inline := $customConfig.config | default dict -}}
{{- $secretName := dig "secretRef" "name" "" $customConfig -}}
{{- $hasInline := gt (len $inline) 0 -}}
{{- $hasSecretRef := ne ($secretName | toString) "" -}}
{{- if and $hasInline $hasSecretRef -}}
{{- fail "spec.oidc.customConfig: set exactly one of `config` (inline) or `secretRef.name` — they are mutually exclusive." -}}
{{- end -}}
{{- if not (or $hasInline $hasSecretRef) -}}
{{- fail "spec.oidc.mode: CustomConfig requires either spec.oidc.customConfig.config (inline generic_oauth map) or spec.oidc.customConfig.secretRef.name (Secret with an `auth.ini` key)." -}}
{{- end -}}
{{- end -}}
| {{- if eq .Values.oidc.mode "System" }} | ||
| {{- $secretName := include "monitoring.oidc.clientSecretName" . }} | ||
| {{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }} | ||
| {{- $clientSecret := "" }} | ||
| {{- if $existing }} | ||
| {{- $clientSecret = index $existing.data "client-secret" | b64dec }} | ||
| {{- else }} | ||
| {{- $clientSecret = randAlphaNum 32 }} | ||
| {{- end }} |
There was a problem hiding this comment.
Use safe navigation to check if .Values.oidc is defined, and use dig to safely retrieve the existing client secret. This prevents nil pointer evaluation errors if oidc is omitted or if the existing Secret is found but lacks the data field or the client-secret key.
{{- $oidc := .Values.oidc | default dict }}
{{- if eq $oidc.mode "System" }}
{{- $secretName := include "monitoring.oidc.clientSecretName" . }}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }}
{{- $clientSecret := "" }}
{{- if $existing }}
{{- $clientSecret = dig "data" "client-secret" "" $existing | b64dec }}
{{- end }}
{{- if eq $clientSecret "" }}
{{- $clientSecret = randAlphaNum 32 }}
{{- end }}| `role_attribute_path` (see grafana.yaml) evaluates group membership | ||
| on login and assigns Admin / Editor / Viewer accordingly. | ||
| */}} | ||
| {{- if eq .Values.oidc.mode "System" }} |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/system/monitoring/tests/oidc_test.yaml (1)
131-148: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRegex doesn't actually verify the claimed 32-char length.
The test is titled "persists a random 32-char client-secret" but
matchRegexonly enforces character set (^[A-Za-z0-9]+={0,2}$), not length. A regression that shortens the random secret (e.g., to 8 chars) would still pass this assertion.💚 Tighten the regex to assert the expected base64 length
- matchRegex: path: data["client-secret"] - pattern: "^[A-Za-z0-9]+={0,2}$" + pattern: "^[A-Za-z0-9]{43}=$"Adjust the exact length/padding to match whatever encoding scheme the template actually uses (base64 of 32 random bytes vs. 32 random alphanumeric chars pre-encoding differ in output length).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/system/monitoring/tests/oidc_test.yaml` around lines 131 - 148, The test assertion for the OIDC client secret only checks allowed characters, not the expected length, so it can miss regressions in the generated secret size. Update the `it: mode=System persists a random 32-char client-secret at install` case in `templates/grafana/oidc-client-secret.yaml`/`matchRegex` to validate the actual encoded length and padding produced by the secret generation scheme, using the existing `data["client-secret"]` assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hack/e2e-apps/monitoring-oidc-system.bats`:
- Around line 89-98: The OIDC client Secret name is being built from the wrong
release name in the e2e test, so the lookup never matches the rendered Secret.
Update the Secret reference in monitoring-oidc-system.bats to use the
HelmRelease name form expected by monitoring.oidc.clientSecretName, and apply
the same name to the matching secretKeyRef assertion so both checks target the
actual created Secret. Use the existing test variables and the
monitoring-${TEST_NAME} release naming convention to locate the affected
assertions.
In `@packages/system/monitoring/templates/_helpers.tpl`:
- Around line 56-70: The allowAssignGrafanaAdmin guard in
monitoring.oidc.allowAssignGrafanaAdmin only checks .Release.Name, so it can
grant GrafanaAdmin to any release named monitoring-system. Update this helper to
require both the expected release name and the cozy-monitoring namespace before
returning true, and keep tenant instances on false. Use the existing
monitoring.oidc.allowAssignGrafanaAdmin symbol as the entry point for the fix.
In `@packages/system/monitoring/templates/grafana/oidc-client-secret.yaml`:
- Around line 25-29: The OIDC secret template’s existing-secret branch in
oidc-client-secret.yaml does not handle a missing client-secret entry, so it can
reuse an empty value instead of generating one. Update the $existing handling to
check that the data map actually contains client-secret before calling
index/b64dec, and fall back to randAlphaNum 32 when the key is absent or empty.
Keep the fix in the template logic that sets $clientSecret so the rendered
secret always gets a valid value.
---
Nitpick comments:
In `@packages/system/monitoring/tests/oidc_test.yaml`:
- Around line 131-148: The test assertion for the OIDC client secret only checks
allowed characters, not the expected length, so it can miss regressions in the
generated secret size. Update the `it: mode=System persists a random 32-char
client-secret at install` case in
`templates/grafana/oidc-client-secret.yaml`/`matchRegex` to validate the actual
encoded length and padding produced by the secret generation scheme, using the
existing `data["client-secret"]` assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 866218e9-e57c-4fd3-bada-4eb6414577b9
📒 Files selected for processing (13)
docs/oidc-grafana.mdhack/e2e-apps/monitoring-oidc-customconfig.batshack/e2e-apps/monitoring-oidc-system.batspackages/extra/monitoring/README.mdpackages/extra/monitoring/values.schema.jsonpackages/extra/monitoring/values.yamlpackages/system/monitoring-rd/cozyrds/monitoring.yamlpackages/system/monitoring/templates/_helpers.tplpackages/system/monitoring/templates/grafana/grafana.yamlpackages/system/monitoring/templates/grafana/oidc-client-secret.yamlpackages/system/monitoring/templates/grafana/oidc-keycloak.yamlpackages/system/monitoring/tests/oidc_test.yamlpackages/system/monitoring/values.yaml
| {{- /* | ||
| Platform Grafana (release name `monitoring-system` in the | ||
| `cozy-monitoring` namespace) is operated by the platform admin and | ||
| should be able to receive `GrafanaAdmin` (server-level) — currently | ||
| we upgrade `Admin` role assignments to `GrafanaAdmin` there via the | ||
| `allow_assign_grafana_admin` flag. Tenant Grafana instances stay at | ||
| org-level Admin. | ||
| */}} | ||
| {{- define "monitoring.oidc.allowAssignGrafanaAdmin" -}} | ||
| {{- if eq .Release.Name "monitoring-system" -}} | ||
| true | ||
| {{- else -}} | ||
| false | ||
| {{- end -}} | ||
| {{- end -}} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Admin-escalation guard doesn't check namespace, only release name.
The doc comment scopes the platform Grafana as release monitoring-system in cozy-monitoring, but the guard only compares .Release.Name. Since release/CR names appear caller-controlled elsewhere in this PR (e.g. e2e tests parametrize the CR name), a tenant naming their own release monitoring-system in their own namespace would satisfy this check and get allow_assign_grafana_admin: true.
🔒 Proposed fix: pin both namespace and release name
{{- define "monitoring.oidc.allowAssignGrafanaAdmin" -}}
-{{- if eq .Release.Name "monitoring-system" -}}
+{{- if and (eq .Release.Namespace "cozy-monitoring") (eq .Release.Name "monitoring-system") -}}
true
{{- else -}}
false
{{- end -}}
{{- end -}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {{- /* | |
| Platform Grafana (release name `monitoring-system` in the | |
| `cozy-monitoring` namespace) is operated by the platform admin and | |
| should be able to receive `GrafanaAdmin` (server-level) — currently | |
| we upgrade `Admin` role assignments to `GrafanaAdmin` there via the | |
| `allow_assign_grafana_admin` flag. Tenant Grafana instances stay at | |
| org-level Admin. | |
| */}} | |
| {{- define "monitoring.oidc.allowAssignGrafanaAdmin" -}} | |
| {{- if eq .Release.Name "monitoring-system" -}} | |
| true | |
| {{- else -}} | |
| false | |
| {{- end -}} | |
| {{- end -}} | |
| {{- /* | |
| Platform Grafana (release name `monitoring-system` in the | |
| `cozy-monitoring` namespace) is operated by the platform admin and | |
| should be able to receive `GrafanaAdmin` (server-level) — currently | |
| we upgrade `Admin` role assignments to `GrafanaAdmin` there via the | |
| `allow_assign_grafana_admin` flag. Tenant Grafana instances stay at | |
| org-level Admin. | |
| */}} | |
| {{- define "monitoring.oidc.allowAssignGrafanaAdmin" -}} | |
| {{- if and (eq .Release.Namespace "cozy-monitoring") (eq .Release.Name "monitoring-system") -}} | |
| true | |
| {{- else -}} | |
| false | |
| {{- end -}} | |
| {{- end -}} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/system/monitoring/templates/_helpers.tpl` around lines 56 - 70, The
allowAssignGrafanaAdmin guard in monitoring.oidc.allowAssignGrafanaAdmin only
checks .Release.Name, so it can grant GrafanaAdmin to any release named
monitoring-system. Update this helper to require both the expected release name
and the cozy-monitoring namespace before returning true, and keep tenant
instances on false. Use the existing monitoring.oidc.allowAssignGrafanaAdmin
symbol as the entry point for the fix.
1970364 to
c458dd7
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/platform/sources/monitoring-application.yaml`:
- Around line 21-29: Move the cozystack.keycloak-operator entry out of the
shared default buildDependsOn for cozystack.monitoring-application and make it
conditional on platform OIDC being enabled. Update the
monitoring-application.yaml dependency definition so the Keycloak requirement is
only applied when authentication.oidc.enabled is true, while keeping the
non-OIDC modes (None/CustomConfig) dependency-free and allowing the package to
become ready.
In `@packages/system/monitoring/templates/_helpers.tpl`:
- Around line 57-66: The `monitoring.oidc.allowAssignGrafanaAdmin` helper still
reads a tenant-controlled `grafanaAdmin` value from `.Values.oidc`, so tenants
can self-enable server-level GrafanaAdmin. Remove `spec.oidc.grafanaAdmin` from
the `Monitoring` CR schema in `monitoring.yaml`, or add validation to reject it,
while keeping the helper usable only from platform-provided values so the flag
remains platform-only.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a5841ec0-a4b9-49ce-918b-a9fa59ea4a0e
📒 Files selected for processing (12)
docs/oidc-grafana.mdpackages/core/platform/sources/monitoring-application.yamlpackages/core/platform/templates/bundles/system.yamlpackages/core/platform/tests/bundles_monitoring_grafana_admin_test.yamlpackages/core/platform/tests/sources_monitoring_application_dependson_test.yamlpackages/extra/monitoring/README.mdpackages/extra/monitoring/values.schema.jsonpackages/extra/monitoring/values.yamlpackages/system/monitoring-rd/cozyrds/monitoring.yamlpackages/system/monitoring/templates/_helpers.tplpackages/system/monitoring/tests/oidc_test.yamlpackages/system/monitoring/values.yaml
✅ Files skipped from review due to trivial changes (2)
- packages/extra/monitoring/README.md
- docs/oidc-grafana.md
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/system/monitoring/values.yaml
- packages/extra/monitoring/values.yaml
- packages/system/monitoring-rd/cozyrds/monitoring.yaml
- packages/extra/monitoring/values.schema.json
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/core/platform/sources/monitoring-application.yaml (1)
21-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider YAML anchors to prevent
libraries/componentsdrift between variants.The
libraries(Lines 21-23, 56-58) andcomponents(Lines 24-35, 59-70) blocks are identical betweendefaultandoidc. This is the same pattern of duplication that led to the prior dependency-gating bug (one variant edited, the other left stale). Anchoring the shared blocks removes that risk going forward (thedependsOnlists differ by one entry, so an anchor/alias doesn't cleanly apply there).♻️ Proposed refactor using YAML anchors
variants: - name: default dependsOn: - cozystack.networking - cozystack.postgres-operator - cozystack.grafana-operator - cozystack.victoria-metrics-operator # cozystack-engine ships the ApplicationDefinition CRD rendered by the *-rd component below. - cozystack.cozystack-engine - libraries: - - name: cozy-lib - path: library/cozy-lib - components: - - name: monitoring-system - path: system/monitoring - libraries: ["cozy-lib"] - - name: monitoring - path: extra/monitoring - libraries: ["cozy-lib"] - - name: monitoring-rd - path: system/monitoring-rd - install: - namespace: cozy-system - releaseName: monitoring-rd + libraries: &monitoringApplicationLibraries + - name: cozy-lib + path: library/cozy-lib + components: &monitoringApplicationComponents + - name: monitoring-system + path: system/monitoring + libraries: ["cozy-lib"] + - name: monitoring + path: extra/monitoring + libraries: ["cozy-lib"] + - name: monitoring-rd + path: system/monitoring-rd + install: + namespace: cozy-system + releaseName: monitoring-rd ... - name: oidc dependsOn: - cozystack.networking - cozystack.postgres-operator - cozystack.grafana-operator - cozystack.victoria-metrics-operator - cozystack.cozystack-engine - cozystack.keycloak-operator - libraries: - - name: cozy-lib - path: library/cozy-lib - components: - - name: monitoring-system - path: system/monitoring - libraries: ["cozy-lib"] - - name: monitoring - path: extra/monitoring - libraries: ["cozy-lib"] - - name: monitoring-rd - path: system/monitoring-rd - install: - namespace: cozy-system - releaseName: monitoring-rd + libraries: *monitoringApplicationLibraries + components: *monitoringApplicationComponents🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/platform/sources/monitoring-application.yaml` around lines 21 - 70, The default and oidc variants duplicate the same libraries/components definitions, which risks drift when one changes and the other is forgotten. Refactor the shared `libraries` and `components` blocks in `monitoring-application.yaml` to use YAML anchors/aliases so both variants reuse the same `cozy-lib`, `monitoring-system`, `monitoring`, and `monitoring-rd` definitions. Keep the `dependsOn` section separate because the `oidc` variant uniquely includes `cozystack.keycloak-operator`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/core/platform/sources/monitoring-application.yaml`:
- Around line 21-70: The default and oidc variants duplicate the same
libraries/components definitions, which risks drift when one changes and the
other is forgotten. Refactor the shared `libraries` and `components` blocks in
`monitoring-application.yaml` to use YAML anchors/aliases so both variants reuse
the same `cozy-lib`, `monitoring-system`, `monitoring`, and `monitoring-rd`
definitions. Keep the `dependsOn` section separate because the `oidc` variant
uniquely includes `cozystack.keycloak-operator`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 313c0ff4-bfa4-4dac-9da4-eae08fc3401b
📒 Files selected for processing (5)
docs/oidc-grafana.mdpackages/core/platform/sources/monitoring-application.yamlpackages/core/platform/templates/bundles/system.yamlpackages/core/platform/tests/bundles_monitoring_grafana_admin_test.yamlpackages/core/platform/tests/sources_monitoring_application_dependson_test.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/core/platform/templates/bundles/system.yaml
- packages/core/platform/tests/sources_monitoring_application_dependson_test.yaml
3e8de1e to
4e29fed
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/system/monitoring/templates/grafana/oidc-keycloak.yaml`:
- Around line 28-30: The Grafana OIDC template currently enters the System mode
path in oidc-keycloak.yaml but silently skips Keycloak resources when the
v1.edp.epam.com/v1 CRD is missing, which can leave Grafana configured without a
working login flow. Update the oidc-keycloak rendering logic around the existing
oidc.assertSystemEnabled and Capabilities.APIVersions.Has check so it either
fails explicitly when the Keycloak CRD is absent or gates the Grafana
auth.generic_oauth config on the same condition; use the oidc-keycloak template
block and the related monitoring.oidc.assertSystemEnabled helper to keep the
behavior consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1411cb23-adba-45dd-ac7e-1c545446b175
📒 Files selected for processing (17)
docs/oidc-grafana.mdhack/e2e-apps/monitoring-oidc-customconfig.batshack/e2e-apps/monitoring-oidc-system.batspackages/core/platform/sources/monitoring-application.yamlpackages/core/platform/templates/bundles/system.yamlpackages/core/platform/tests/bundles_monitoring_grafana_admin_test.yamlpackages/core/platform/tests/sources_monitoring_application_dependson_test.yamlpackages/extra/monitoring/README.mdpackages/extra/monitoring/values.schema.jsonpackages/extra/monitoring/values.yamlpackages/system/monitoring-rd/cozyrds/monitoring.yamlpackages/system/monitoring/templates/_helpers.tplpackages/system/monitoring/templates/grafana/grafana.yamlpackages/system/monitoring/templates/grafana/oidc-client-secret.yamlpackages/system/monitoring/templates/grafana/oidc-keycloak.yamlpackages/system/monitoring/tests/oidc_test.yamlpackages/system/monitoring/values.yaml
✅ Files skipped from review due to trivial changes (1)
- packages/extra/monitoring/README.md
🚧 Files skipped from review as they are similar to previous changes (13)
- packages/core/platform/tests/bundles_monitoring_grafana_admin_test.yaml
- packages/core/platform/sources/monitoring-application.yaml
- packages/core/platform/templates/bundles/system.yaml
- packages/extra/monitoring/values.schema.json
- packages/system/monitoring/templates/grafana/grafana.yaml
- packages/system/monitoring-rd/cozyrds/monitoring.yaml
- packages/extra/monitoring/values.yaml
- packages/core/platform/tests/sources_monitoring_application_dependson_test.yaml
- packages/system/monitoring/values.yaml
- packages/system/monitoring/tests/oidc_test.yaml
- hack/e2e-apps/monitoring-oidc-system.bats
- packages/system/monitoring/templates/_helpers.tpl
- hack/e2e-apps/monitoring-oidc-customconfig.bats
660a02f to
f685300
Compare
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
The Phase-1 core here is faithful to the shape settled in cozystack/community#24 and #3044: the None | System | CustomConfig selector, the per-instance confidential client with the audience mapper, direct-trust CustomConfig with cozy out of the path, the XOR and platform-flag fail-close guards, and the deferred GrafanaAdmin lever are all right, and the tests around them are solid. I'm requesting changes on one architectural block — the RBAC surface of System mode, i.e. the three chart-owned KeycloakRealmGroups and the role_attribute_path that consumes them — for three reasons that stack.
1. A Helm release must not own directory objects
Phase 1 of cozystack/community#24 landed on "couple at provisioning, decouple at ownership": a service may request identity wiring, but IdP-object lifecycle must not be tied to an app-level flag. That's why #3044 ships with zero auto-provisioned Keycloak groups — authentication comes from cozy, and authorization is a users: map reconciled into ClusterRoleBindings inside the thing being authorized.
Here, setting spec.oidc.mode: System → None (or deleting the Monitoring release) Helm-deletes three realm groups, and with them the membership someone curated out-of-band — directory state destroyed by toggling a monitoring value. Per-service groups in the platform realm are exactly the question left open at the end of the community#24 thread (the per-instance-groups lean was never resolved there). If chart-owned groups are to become a pattern, that needs to be settled in the design proposal first, not established as precedent by a monitoring PR.
2. Dash-separated group names are ambiguous by construction
#2019 moved Cozystack's RBAC resource names to the k8s-style colon scheme (cozy:tenant:admin) precisely because dash-concatenation of segments that themselves contain dashes is not injective. These groups are <namespace>-<release>-<role>, where the namespace is itself a dash-joined tenant path and the release is always monitoring-system — so collisions are constructible, not hypothetical:
- Tenant
acmeopts in → its Grafana admin group istenant-acme-monitoring-system-admin. - Someone creates subtenant
monitoringunderacme, and subtenantsystemunder that → the tenant chart provisions realm grouptenant-acme-monitoring-system-adminas that namespace's tenant-admin group.
Same flat-realm group name, two unrelated owners (two KeycloakRealmGroup CRs in different namespaces reconciling one group) and two unrelated grants (tenant-admin of tenant-acme-monitoring-system; org-Admin on acme's Grafana). Membership in one is membership in both. If groups survive point 1 at all, the naming must be collision-free by construction — these groups feed only Grafana's JMESPath, not Kubernetes RBAC, so a separator that cannot appear in a segment (the #2019 colon scheme) is available here.
3. Grafana already has the app-side authz surface — the equivalent of the CRBs in #3044
Grafana org roles are first-class, persistent, API-manageable state. The design that actually mirrors #3044 is:
spec.oidc.users: [{email, role: Admin | Editor | Viewer}]on the Monitoring CR;- a chart-owned post-install/post-upgrade hook Job reconciling that map into org membership/roles via Grafana's admin API (
/api/orgs/:orgId/users), authenticated with theadmin_user/admin_passwordSecret the chart already owns — the same vehicle and roughly the same code shape as #3044's bootstrap Job (grafana-operator has no user CRD, so the operator can't do this declaratively); skip_org_role_sync = truein thegeneric_oauthblock, so a login never overwrites app-side assignments;- no
KeycloakRealmGroups, norole_attribute_path.
That gives: no directory objects owned by the release lifecycle, no naming collisions, the same operator UX as the Kubernetes sibling — a genuine one-mental-model story across both PRs, which is this PR's stated goal.
Related, and fixed for free by point 3: the default-Viewer fallback
monitoring.oidc.roleAttributePath ends in || 'Viewer', and role_attribute_strict is not set. The audience mapper prevents token replay, but it does not gate who can complete the login flow: by default every user of the flat cozy realm can log in against this client, and the fallback then hands each of them org-Viewer — cross-tenant read access to metrics for the entire platform directory, in every opted-in Grafana. Under the users-map approach, unmapped users simply get nothing. If the group mapping is kept, this needs role_attribute_strict = true and no fallback, at minimum.
Smaller items (only relevant if the group design stays)
docs/oidc-grafana.md'sKeycloakRealmUserexample uses grouptenant-acme-monitoring-admin, but the identity unit is the inner release (alwaysmonitoring-system), so the rendered group istenant-acme-monitoring-system-admin— an operator following the doc verbatim creates a group that maps to no role. The_helpers.tplheader example (tenant-foo/monitoring) is stale the same way; the bats were already fixed for this.oidc-client-secret.yaml:index $existing.data "client-secret" | b64dechas no fallback when an existing Secret lacks the key (already flagged by CodeRabbit).
Everything outside the RBAC block — client + audience scope, secret persistence and lookup preservation, CustomConfig XOR and mount wiring, the CRD/platform-flag guards, the default/oidc variant split in the platform bundle — looks ready.
a48b17d to
0fd5751
Compare
…p + reconcile Job Replace the group-based authorization model on the Phase-1 Grafana OIDC selector with an app-side users map, mirroring the tenant kube-apiserver sibling in #3044. What is removed - The three chart-owned KeycloakRealmGroup objects in the cozy realm (`<clientId>-{admin,editor,viewer}`). A Helm release must not own directory objects — toggling `spec.oidc.mode: System -> None` or deleting the Monitoring release would Helm-delete the groups, and with them any membership curated out-of-band. Per-instance groups in the platform realm is exactly the design question left open at the end of cozystack/community#24; establishing it as precedent from a monitoring PR is out of scope. - Dash-separated group naming (`<namespace>-<release>-<role>`) was also collision-prone against tenant/subtenant paths, since both the namespace and release names can themselves contain dashes. - `role_attribute_path` in the Grafana `generic_oauth` block and the default-Viewer fallback, which handed org-Viewer to every cozy realm identity that logged in — cross-tenant metrics read across every opted-in Grafana. Also removed the `groups` client scope from the KeycloakClient defaults. - `monitoring.oidc.allowAssignGrafanaAdmin` helper and the internal `oidc.grafanaAdmin` values key: the feature was already deferred and the helper's release-name-only guard was tenant-spoofable. What replaces it - `spec.oidc.users: [{email, role: Admin|Editor|Viewer}]` on the Monitoring chart values. - A chart-owned post-install/post-upgrade Job (`templates/grafana/oidc-users-job.yaml`) that reconciles that list into Grafana's Main Org. via the admin API: pre-provisions a local Grafana account per listed email so the reconcile can set the org role before the operator's first OIDC login, adds the account to the org, PATCHes the role to converge across re-runs, and prunes every non-admin Main-Org member whose email is not in the list (handles both entry removal and `mode: None`). - `skip_org_role_sync = true` + `oauth_allow_insecure_email_lookup = true` in the generic_oauth block. The former stops a login from overwriting the Job's assignments; the latter binds the OIDC identity to the pre-provisioned local account. - Job talks to Grafana over the in-cluster Service and reads admin creds from the chart-managed `grafana-admin-password` Secret via `envFrom`: no ServiceAccount / Role / RoleBinding. `activeDeadlineSeconds: 900` caps a stuck Job so a broken Grafana cannot hold the release in `pending-upgrade`. Defensive rendering - `.Values.oidc | default dict` in the four templates that dereference the `oidc` block so `oidc: ~` or an omitted block no longer nil-derefs. - `oidc-client-secret.yaml` falls back to a fresh random when the existing Secret lookup succeeds but `data.client-secret` is empty. Rationale: #3176 review from @lllamnyp (architectural CHANGES_REQUESTED points 1, 2, 3 + the default-Viewer leak). The three points stack: dropping the groups incidentally fixes the naming-collision and the default-Viewer leak, and the users-Job gives operators the same UX story as the tenant kube-apiserver PR across both sides of the platform. Signed-off-by: IvanHunters <[email protected]>
…p + reconcile Job Replace the group-based authorization model on the Phase-1 Grafana OIDC selector with an app-side users map, mirroring the tenant kube-apiserver sibling in #3044. What is removed - The three chart-owned KeycloakRealmGroup objects in the cozy realm (`<clientId>-{admin,editor,viewer}`). A Helm release must not own directory objects — toggling `spec.oidc.mode: System -> None` or deleting the Monitoring release would Helm-delete the groups, and with them any membership curated out-of-band. Per-instance groups in the platform realm is exactly the design question left open at the end of cozystack/community#24; establishing it as precedent from a monitoring PR is out of scope. - Dash-separated group naming (`<namespace>-<release>-<role>`) was also collision-prone against tenant/subtenant paths, since both the namespace and release names can themselves contain dashes. - `role_attribute_path` in the Grafana `generic_oauth` block and the default-Viewer fallback, which handed org-Viewer to every cozy realm identity that logged in — cross-tenant metrics read across every opted-in Grafana. Also removed the `groups` client scope from the KeycloakClient defaults. - `monitoring.oidc.allowAssignGrafanaAdmin` helper and the internal `oidc.grafanaAdmin` values key: the feature was already deferred and the helper's release-name-only guard was tenant-spoofable. What replaces it - `spec.oidc.users: [{email, role: Admin|Editor|Viewer}]` on the Monitoring chart values. - A chart-owned post-install/post-upgrade Job (`templates/grafana/oidc-users-job.yaml`) that reconciles that list into Grafana's Main Org. via the admin API: pre-provisions a local Grafana account per listed email so the reconcile can set the org role before the operator's first OIDC login, adds the account to the org, PATCHes the role to converge across re-runs, and prunes every non-admin Main-Org member whose email is not in the list (handles both entry removal and `mode: None`). - `skip_org_role_sync = true` + `oauth_allow_insecure_email_lookup = true` in the generic_oauth block. The former stops a login from overwriting the Job's assignments; the latter binds the OIDC identity to the pre-provisioned local account. - Job talks to Grafana over the in-cluster Service and reads admin creds from the chart-managed `grafana-admin-password` Secret via `envFrom`: no ServiceAccount / Role / RoleBinding. `activeDeadlineSeconds: 900` caps a stuck Job so a broken Grafana cannot hold the release in `pending-upgrade`. Defensive rendering - `.Values.oidc | default dict` in the four templates that dereference the `oidc` block so `oidc: ~` or an omitted block no longer nil-derefs. - `oidc-client-secret.yaml` falls back to a fresh random when the existing Secret lookup succeeds but `data.client-secret` is empty. Rationale: #3176 review from @lllamnyp (architectural CHANGES_REQUESTED points 1, 2, 3 + the default-Viewer leak). The three points stack: dropping the groups incidentally fixes the naming-collision and the default-Viewer leak, and the users-Job gives operators the same UX story as the tenant kube-apiserver PR across both sides of the platform. Signed-off-by: IvanHunters <[email protected]>
c703e5f to
83fcbca
Compare
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
The users-map rework lands the design cleanly: the KeycloakRealmGroups and role_attribute_path are gone, System mode provisions only the client + audience scope + persisted secret, and the hook Job (pre-provision by email → add to Main Org → PATCH role → prune) is the right shape, with skip_org_role_sync and oauth_allow_insecure_email_lookup correctly chart-forced so the contract survives CustomConfig-inline overrides. The mode: None carve-out for the prune pass — protecting manually-added users on pre-OIDC installs — is exactly the right instinct. Two gaps remain, both small fixes; requesting changes because the first is the isolation property this design exists to provide.
1. allow_sign_up is missing from the chart-forced dict — unmapped users still get Viewer
docs/oidc-grafana.md now states: "Users not listed in spec.oidc.users who log in through OIDC get nothing — no default Viewer role, no cross-tenant read access." The chart doesn't render that behavior. allow_sign_up is set nowhere, and Grafana's [auth.generic_oauth] default is allow_sign_up = true; with skip_org_role_sync = true a fresh OAuth signup receives auto_assign_org_role, whose default is Viewer. So any cozy-realm identity can still complete the login flow between upgrades, get a new account with org-Viewer, and keep it until the next helm upgrade runs the prune hook.
The machinery that makes the fix safe is already in place: the Job pre-provisions a local account for every mapped user, and oauth_allow_insecure_email_lookup = true binds the OIDC identity to that account at login. That combination exists precisely so sign-up can be closed. Add allow_sign_up: "false" to $chartForcedOidc in templates/grafana/grafana.yaml, and the docs sentence becomes true: unmapped users are rejected at the door instead of admitted as Viewer and reaped later.
2. The prune pass with an empty users list reaps the whole org — including in secretRef mode, where the chart promises to be hands-off
oidc-users-job.yaml renders whenever mode != None, and the prune removes every non-admin Main-Org member whose email isn't in the desired list. But assertCustomSecretRefUsersEmpty forbids users under customConfig.secretRef — so in that mode the desired list is always empty, and every upgrade deletes every non-admin org member. That contradicts both "the operator's mounted auth.ini is authoritative" and the assert's own error message, which tells the operator to "unset users and manage authorization inside the ini fragment yourself" — the Job then deletes the members they manage that way. The same wipe happens in System or CustomConfig-inline with users simply unset.
The mode: None guard shows this failure class was seen — it just needs to cover the other empty-list doors too. Suggestion: don't render the Job when users is empty (this covers secretRef automatically, since users can't be set there), and state in the docs that a non-empty users map means the chart owns Main-Org membership — anything added by hand gets pruned on the next reconcile. The trade-off (removing the last users entry no longer prunes it) is worth a sentence in the docs; going users: [...] → users: [] → cleanup-by-hand matches the already-documented System → None posture.
Minor
- With fix 2 in place, consider forcing
oauth_allow_insecure_email_lookup(andallow_sign_up: "false"from fix 1) only whenusersis non-empty. A CustomConfig-inline operator who doesn't use the users map at all currently gets insecure email lookup forced on with nothing to bind — combined with a BYO IdP that doesn't verify emails, that's a needless account-linking surface in their instance. - The E2E failure on the current head is the
etcdtest (etcd-operator learner-promotion flake inherited from the rebase), not this PR's suites — bothmonitoring-oidc-*bats passed.
Everything else from the previous round is closed: the client-secret lookup is key-guarded, the nil-safe $oidc navigation landed, and the docs/test counts moved with the design.
…p + reconcile Job Replace the group-based authorization model on the Phase-1 Grafana OIDC selector with an app-side users map, mirroring the tenant kube-apiserver sibling in #3044. What is removed - The three chart-owned KeycloakRealmGroup objects in the cozy realm (`<clientId>-{admin,editor,viewer}`). A Helm release must not own directory objects — toggling `spec.oidc.mode: System -> None` or deleting the Monitoring release would Helm-delete the groups, and with them any membership curated out-of-band. Per-instance groups in the platform realm is exactly the design question left open at the end of cozystack/community#24; establishing it as precedent from a monitoring PR is out of scope. - Dash-separated group naming (`<namespace>-<release>-<role>`) was also collision-prone against tenant/subtenant paths, since both the namespace and release names can themselves contain dashes. - `role_attribute_path` in the Grafana `generic_oauth` block and the default-Viewer fallback, which handed org-Viewer to every cozy realm identity that logged in — cross-tenant metrics read across every opted-in Grafana. Also removed the `groups` client scope from the KeycloakClient defaults. - `monitoring.oidc.allowAssignGrafanaAdmin` helper and the internal `oidc.grafanaAdmin` values key: the feature was already deferred and the helper's release-name-only guard was tenant-spoofable. What replaces it - `spec.oidc.users: [{email, role: Admin|Editor|Viewer}]` on the Monitoring chart values. - A chart-owned post-install/post-upgrade Job (`templates/grafana/oidc-users-job.yaml`) that reconciles that list into Grafana's Main Org. via the admin API: pre-provisions a local Grafana account per listed email so the reconcile can set the org role before the operator's first OIDC login, adds the account to the org, PATCHes the role to converge across re-runs, and prunes every non-admin Main-Org member whose email is not in the list (handles both entry removal and `mode: None`). - `skip_org_role_sync = true` + `oauth_allow_insecure_email_lookup = true` in the generic_oauth block. The former stops a login from overwriting the Job's assignments; the latter binds the OIDC identity to the pre-provisioned local account. - Job talks to Grafana over the in-cluster Service and reads admin creds from the chart-managed `grafana-admin-password` Secret via `envFrom`: no ServiceAccount / Role / RoleBinding. `activeDeadlineSeconds: 900` caps a stuck Job so a broken Grafana cannot hold the release in `pending-upgrade`. Defensive rendering - `.Values.oidc | default dict` in the four templates that dereference the `oidc` block so `oidc: ~` or an omitted block no longer nil-derefs. - `oidc-client-secret.yaml` falls back to a fresh random when the existing Secret lookup succeeds but `data.client-secret` is empty. Rationale: #3176 review from @lllamnyp (architectural CHANGES_REQUESTED points 1, 2, 3 + the default-Viewer leak). The three points stack: dropping the groups incidentally fixes the naming-collision and the default-Viewer leak, and the users-Job gives operators the same UX story as the tenant kube-apiserver PR across both sides of the platform. Signed-off-by: IvanHunters <[email protected]>
Extends the tenant-facing `Monitoring` CR with the new
`spec.oidc.users: [{email, role: Admin|Editor|Viewer}]` field
consumed by the users-Job introduced in the previous commit. Works
for both `System` and `CustomConfig` modes; ignored when
`mode: None`.
Regenerated `packages/extra/monitoring/values.schema.json`,
`README.md`, and the mirrored `openAPISchema` on the
`monitoring-rd` ApplicationDefinition through the standard
`make generate` (cozyvalues-gen + hack/update-crd.sh) pipeline.
Also refreshed the stale annotation comment on the CR that still
claimed the chart provisions three KeycloakRealmGroups.
Signed-off-by: IvanHunters <[email protected]>
…roups Update the oidc-variant comment on `packages/core/platform/sources/monitoring-application.yaml` so it no longer claims `spec.oidc.mode: System` renders three KeycloakRealmGroups. The dependsOn edge and the fail-close guard in `templates/grafana/oidc-keycloak.yaml` stay unchanged: the KeycloakClient + KeycloakClientScope path still needs the `v1.edp.epam.com` CRDs from `cozystack.keycloak-operator` to be registered before the monitoring chart reconciles. Signed-off-by: IvanHunters <[email protected]>
Rewrites the helm-unittest coverage for the Phase-1 OIDC selector
against the new chart contract:
- Drop assertions for the three KeycloakRealmGroups and for
`role_attribute_path` / `allow_assign_grafana_admin` / the
`oidc.grafanaAdmin` values lever — none of those render anymore.
- Assert the System-mode Grafana config carries
`skip_org_role_sync = "true"` and
`oauth_allow_insecure_email_lookup = "true"` and NO
`role_attribute_path`.
- Cover the new users-Job template: helm-hook annotations, absence
of a ServiceAccount, `activeDeadlineSeconds`, admin-credentials
wiring from the chart-managed `grafana-admin-password` Secret,
and correct serialisation of `spec.oidc.users` into the
`DESIRED_USERS_JSON` env.
- Assert the users-Job also renders under `mode: None` so a
System -> None flip still gets a prune pass.
- Cover the defensive `.Values.oidc | default dict` paths — omitted
block and explicit `oidc: ~` — across the three OIDC templates.
- Tighten the client-secret regex to the actual base64-encoded
length (`^[A-Za-z0-9+/]{43}=$`) so shortened random secrets are
caught.
32 tests, all green under helm-unittest v1.0.3.
Signed-off-by: IvanHunters <[email protected]>
Aligns the render-side e2e bats with the users-map contract: - Feed a `spec.oidc.users:` entry to each mode's Monitoring CR so the reconcile Job has real input to act on. - Assert the users-Job appears in the release namespace with the desired `DESIRED_USERS_JSON` env and the expected `OIDC_MODE`. - Assert the Grafana CR renders with `skip_org_role_sync = "true"` and `oauth_allow_insecure_email_lookup = "true"` and NO `role_attribute_path`. - Flip the three KeycloakRealmGroup existence assertions to negative checks: chart-owned realm groups must NOT be present in either System or CustomConfig mode. Does not add a full OIDC login flow through Keycloak — that stays deferred to a follow-up integration suite, same posture as the tenant kube-apiserver's kubernetes-oidc-system.bats. Signed-off-by: IvanHunters <[email protected]>
…te-path design Rewrite `docs/oidc-grafana.md` for the new authorization model: - Selector example carries a `users:` map. - System-mode "what the chart provisions" list drops the three KeycloakRealmGroups + role_attribute_path, adds the audience scope-only KeycloakClient defaults, and adds the users-Job step with `skip_org_role_sync` + `oauth_allow_insecure_email_lookup` wiring notes. - Fresh "Users and RBAC" section describes the users-Job reconcile contract: pre-provision -> add-to-org -> PATCH role -> prune. Explains the "user not in list -> gets nothing" semantics (replaces the default-Viewer fallback). - CustomConfig section explicitly notes that `role_attribute_path` in the operator's payload is now dead config: the chart forces `skip_org_role_sync = true` regardless of mode. - "Failure modes" gains "user logs in but sees no dashboards" (missing from `users:`) and a users-Job troubleshooting entry. - Drop the `GrafanaAdmin` deferral notes and the "Mode toggle destroys chart-owned KeycloakRealmGroup membership" failure mode — both moot now that the groups are gone. - Removed the stale `KeycloakRealmUser` example that used `tenant-acme-monitoring-admin` (the chart never rendered a group by that name — the inner release is `monitoring-system` and the example was a live footgun). Signed-off-by: IvanHunters <[email protected]>
…ry OIDC mode
The chart previously injected the two settings the users-Job's
contract depends on — `skip_org_role_sync = true` and
`oauth_allow_insecure_email_lookup = true` — only inside the
`mode: System` branch of `grafana.yaml`. `values.yaml` and
`docs/oidc-grafana.md` both promised the settings would be forced
regardless of mode, but under `mode: CustomConfig` the operator's
inline `auth.generic_oauth` map was written verbatim, both
settings were absent, and the users-Job's role assignments were
silently stranded (no lookup by email → new Grafana account minted
at login) or transiently overwritten (no skip_org_role_sync → any
`role_attribute_path` or Grafana's `auto_assign_org_role: Viewer`
overwrote the Job's role on each login).
Fix: merge a `$chartForcedOidc` dict on top of both the generated
System block and the operator's inline map (`merge` wins for
overlapping keys, so the operator can NOT override the two
settings). CustomConfig secretRef mode cannot receive the merge
because the ini fragment is authoritative — the chart now hard-fails
render when both `customConfig.secretRef.name` and non-empty
`spec.oidc.users` are set, forcing the operator to either switch to
inline config or manage authorization themselves inside the ini.
Also:
- Align the inner HR `timeout` on `packages/extra/monitoring/templates/helmrelease.yaml`
from 10m to 15m so it matches the outer cozyrds
`release.cozystack.io/helm-install-timeout: "15m"` and does not
fail while the users-Job is still inside its
`activeDeadlineSeconds: 900`.
- Route the users-Job image through `cozy-lib.image` so
mirrored / air-gapped installations resolve it (matches the
pattern in `packages/apps/mariadb/templates/hooks/cleanup-pvc.yaml`).
- Add `spec.ttlSecondsAfterFinished: 3600` on the users-Job so a
succeeded Job + Pod are garbage-collected on the next reconcile
interval rather than lingering until the next hook activation
(which never fires on release uninstall).
- Harden the users-Job shell:
* emails read NUL-terminated (`jq --raw-output0`) so an address
containing whitespace cannot corrupt the `for email in ...`
loop's field splitting;
* `/api/users/lookup?loginOrEmail=<email>` URL-encoded via
`jq @uri` so `&`/`=`/`?` in the local part parse correctly on
the Grafana side;
* prune loop switched from pipe-delimited fields to TSV inside
NUL-terminated records, with `cut -f` for extraction.
- Add a template-level assertion (`monitoring.oidc.assertUsersEmailShape`)
rejecting emails that do not match a conservative regex —
cheaper failure mode than a 400 from Grafana.
Signed-off-by: IvanHunters <[email protected]>
…unittest Two blockers on the previous review cycle were "code says X but no test enforces X" — so future edits to `grafana.yaml` could silently break the contract again. Add three assertions that fail loudly if the merge behavior regresses: - `mode=CustomConfig inline preserves the operator's map + chart-forced settings` — asserts `skip_org_role_sync=true` and `oauth_allow_insecure_email_lookup=true` are both present alongside the operator's `client_id` / `client_secret` / `auth_url`. - `mode=CustomConfig inline — operator cannot override chart-forced OIDC settings` — sets both settings to "false" in the operator's map; asserts they render as "true" (proves `merge` argument order is correct). - `mode=CustomConfig secretRef + non-empty users fails the render (contract incompat)` — asserts the new `monitoring.oidc.assertCustomSecretRefUsersEmpty` helper's error message fires so operators cannot silently strand their users-Job assignments by combining incompatible fields. - `mode=CustomConfig secretRef + empty users renders successfully` — the compatible path. Signed-off-by: IvanHunters <[email protected]>
Previous docs claimed "the chart forces skip_org_role_sync = true regardless of mode" — a promise the code did not keep in CustomConfig mode. The chart is now fixed; sync the docs so the guarantee reads truthfully and the incompatibility surface is explicit: - `docs/oidc-grafana.md`: rewrite the CustomConfig section so both chart-forced settings are named explicitly, the `merge` semantics are documented (operator can't override), and the `secretRef` + `users` incompatibility is called out with an explicit failure path. - `system/monitoring/values.yaml`: expand the `config`/`secretRef` comments so operators reading the values doc see the merge behavior and the secretRef limitation before they hit the render-time fail. Add `enabled: "true"` to the "typical keys" list — Grafana defaults `enabled: false` and an inline map without it is inert. - `extra/monitoring`: regenerated values.schema.json + README.md via `make generate` (cozyvalues-gen + hack/update-crd.sh) to pick up the `oidc.users[].email` doc-string touch; mirrored openAPISchema on the ApplicationDefinition follows. Signed-off-by: IvanHunters <[email protected]>
…chema
The `OIDCUser` typedef in `packages/extra/monitoring/values.yaml`
declared only `@field {string} email`; `cozyvalues-gen` regenerated
`values.schema.json`, `README.md`, and the mirrored `openAPISchema`
on the `Monitoring` ApplicationDefinition WITHOUT a `role` field.
Consequences:
- The `@enum OIDCRole` declaration was orphan — referenced from
nowhere, so the schema's items object had `required: ["email"]`
and one property.
- A dashboard-driven CR would offer a form without any role picker;
a schema-conformant `{email}` submission would render into the
users-Job with `role: null`, `jq --raw-output` yields the string
`"null"`, the `POST /api/orgs/1/users` body carries
`"role":"null"`, Grafana returns HTTP 400, and the post-install
hook exhausts `backoffLimit: 6` — HelmRelease never becomes Ready.
- Unit + e2e tests all hardcoded `role: Admin` / `role: Viewer`, so
the contract mismatch never surfaced in CI.
Fix:
- Add `@field {OIDCRole} role - ...` to the `OIDCUser` typedef in
`packages/extra/monitoring/values.yaml` so cozyvalues-gen materialises
the field with enum + required.
- Regenerate `values.schema.json`, `README.md`, and mirrored
`openAPISchema` via `make generate`.
- Add `monitoring.oidc.assertUsersRoleShape` template-level guard
(same shape as `assertUsersEmailShape`) so a stale-schema CR or a
direct-helm invocation fails at render time with a legible error
instead of at hook execution with a Grafana 400.
- Consolidate all `spec.oidc.users` guards into a single
`monitoring.oidc.assertUsersPrologue` include and call it once from
`oidc-users-job.yaml`, ordering the incompat check
(`assertCustomSecretRefUsersEmpty`) before the per-entry role /
email regexes so operators see the more actionable message first.
Drop the duplicate include from `grafana.yaml` (Job template renders
in every mode anyway).
- Two new helm-unittest cases lock the regression:
`oidc.users entry missing role fails the render` and
`oidc.users entry with unknown role fails the render`.
- Point the previous `secretRef + users` incompat unit test at the
users-Job template since that's where the prologue now fires.
- `docs/oidc-grafana.md`: correct the failed-hook post-mortem window
claim — with `ttlSecondsAfterFinished: 3600` the Kubernetes TTL
controller reaps the Job + Pod 1h after the terminal condition,
not "until the next helm-upgrade".
Signed-off-by: IvanHunters <[email protected]>
The failed-hook post-mortem paragraph in the users-Job troubleshooting section still said the Job "stays around until the next helm-upgrade for post-mortem", but the shipped Job spec carries `ttlSecondsAfterFinished: 3600` — the Kubernetes TTL controller reaps the Job + Pod one hour after the terminal Failed condition regardless of when the next `helm upgrade` lands. Reword to name the 1h window explicitly and instruct the operator to re-trigger the hook if they missed the window. The edit was authored alongside commit 1f66456 but slipped out of that commit's file list; this is the cleanup. Signed-off-by: IvanHunters <[email protected]>
…CR existence CI run 28794778739 caught two race conditions in the render-side Monitoring OIDC bats: - `Grafana CR carries auth.generic_oauth pointing at the cozy realm` (monitoring-oidc-system.bats:86) failed with `enabled=` empty after a 0-second wait. The prior `until kubectl get grafana grafana` only proved the CR EXISTS — it did not prove grafana-operator had reconciled the mode=System values onto it. When the CR from the previous `Monitoring CR accepts spec.oidc.mode=System` test was already there but the operator hadn't yet flipped `.spec.config.auth .generic_oauth.enabled` to "true", the jsonpath returned empty and `[ "" = "true" ]` short-circuited the whole test. - `secretRef variant mounts operator Secret under /etc/grafana/oidc` (monitoring-oidc-customconfig.bats:119) timed out at 180s waiting for the volumeMount swap from inline → ini. On a warm sandbox the full inner-HR upgrade + grafana-operator reconcile cycle can legitimately take >3m — the timeout was too tight. Fix: replace `until kubectl get grafana` with `until [ "$(kubectl ... jsonpath=<target-field>)" = <expected> ]` on every subsequent assertion, and bump the secretRef-swap window to 600s (well inside the inner HR's 15m timeout). Same pattern used for the "no role_attribute_path" and "operator payload verbatim" assertions so the whole suite is race-free. Signed-off-by: IvanHunters <[email protected]>
Caught by dev3 end-to-end test on the PR working tree
(cozystack-pr-test skill): the users-Job was rendered in every mode
including `None` on the rationale that a `mode: System → None`
switch should garbage-collect OIDC-provisioned local accounts. That
rationale ignored the upgrade path from the pre-PR chart: an
existing tenant that never had `spec.oidc` on their Monitoring CR
would receive `mode: None` on first render under the PR, the Job
would fire on every Flux reconcile of the outer HR, and its prune
pass would DELETE every non-admin Main-Org member — including users
the tenant added by hand through the Grafana UI on the pre-PR chart.
Silent breaking change with data loss.
Wrap the whole Job resource in `{{- if ne $mode "None" }} … {{- end }}`
so `mode: None` = "chart owns nothing in Grafana orgs, we don't
touch what's there". Operators who need to prune OIDC-provisioned
accounts after switching System | CustomConfig → None do it
themselves through the Grafana UI or admin API — same posture as
"we don't own the Keycloak directory objects" from the earlier
architectural change.
The `assertUsersPrologue` include stays outside the `if` so an
operator listing users in `mode: None` still gets the schema and
email-shape validation at render time — the Job body just doesn't
emit.
Test coverage:
- Flip the previous "mode=None still renders the users-Job so a
mode switch can prune stale members" case to
`mode=None does NOT render the users-Job (upgrade safety guard)`
as a regression lock.
- Add `mode=None with non-empty users still fails render` covering
the prologue-only-fires-on-schema-issue path.
Docs: rewrite the "Users and RBAC" section step 3 to name the
upgrade-safety motivation and the manual-cleanup handoff.
Signed-off-by: IvanHunters <[email protected]>
…pat) CI E2E caught the Job container crash-looping within 20s of the first Start event. Root cause: the `alpine/k8s:1.33.4` image is Alpine-based (BusyBox coreutils + BusyBox grep), and the Job shell used four GNU-specific constructs that BusyBox does not implement: - `head --bytes=N` (BusyBox `head` only accepts `-c N`). - `tr --delete` (BusyBox `tr` only accepts `-d`). - `jq --raw-output0` (NUL-terminated output — jq 1.7+ feature; also paired with `read -r -d ''` which is a bash-ism, not POSIX `sh` and not honoured by BusyBox `ash`). - `grep --null-data --fixed-strings --line-regexp` (BusyBox `grep` supports `-F` and `-x` short forms but NOT `-z`/`--null-data`). Rewrite: - `head -c 24 /dev/urandom | base64 | tr -d '=+/' | head -c 32` — same random-32-char password, portable to BusyBox. - Emit desired emails and org membership one record per line (jq `--raw-output`) instead of NUL-terminated. Safe because the template-level `monitoring.oidc.assertUsersEmailShape` guard already rejects any email containing whitespace / control characters, so line-oriented iteration cannot get confused. - Read TSV membership records with `IFS="$(printf '\t')"` (POSIX- portable TAB) + plain `read -r uid ulogin uemail` — no need for `-d ''` once records are newline-terminated. - Match desired list with `grep -Fxq -e "$uemail" -e "$ulogin"` — short-form flags supported by both GNU and BusyBox `grep`. Syntax validated with both `sh -n` and `bash -n` on the extracted inline script. helm-unittest 34/34 still green (render shape is unchanged, only the shell literal is different). Signed-off-by: IvanHunters <[email protected]>
…st Job existence
CI E2E showed the users-Job assertion returning `DESIRED_USERS_JSON:
[]` — but the CR body had `users: [{email: [email protected],
role: Admin}]`. Root cause: `hook-delete-policy: before-hook-creation`
keeps the previous hook Job around across helm upgrades, so `until
kubectl get job` matches a stale Job from the previous customconfig-
mode test where users may have been unset (the secretRef swap CR had
no `users:` field, so the schema default `[]` propagated to the
prior Job's DESIRED_USERS_JSON). The System test then grabbed that
stale Job before the helm upgrade for the new CR completed and swapped
in the fresh Job.
Fix: poll on the target env value itself — the new Job with the
correct users list is the first Job whose DESIRED_USERS_JSON string
contains `[email protected]`. 600s window matches the inner HR's
15m timeout with room for a slow reconcile.
Signed-off-by: IvanHunters <[email protected]>
… users-map Address PR#3176 review round-2 gap-1: the docs promise "users not in spec.oidc.users get nothing", but Grafana's default allow_sign_up=true combined with the chart-forced skip_org_role_sync=true was minting a Viewer account for every cozy-realm identity that completed the OAuth flow, then reaping it on the next helm-upgrade prune pass. The chart-forced isolation dict (now three keys: skip_org_role_sync, oauth_allow_insecure_email_lookup, allow_sign_up=false) is applied only when spec.oidc.users is non-empty. A BYO-IdP operator who does not use the users-map gets none of the three forced on top of their inline config, matching the review's minor: forcing insecure_email_lookup against an issuer that may not verify emails is a needless account- linking surface, and allow_sign_up=false with no pre-provisioned accounts would lock everyone out. Tests split the two branches: users-active asserts all three keys are present and the operator cannot override them via CustomConfig-inline; users-empty asserts the auth.generic_oauth block is emitted without any of the isolation keys. Signed-off-by: IvanHunters <[email protected]>
Address PR#3176 review round-2 gap-2: the users-Job's prune pass deletes every non-admin Main-Org member whose email is not in the desired list. When the desired list is empty every helm-upgrade wiped the whole org. Three empty-users states used to fall into this trap: * customConfig.secretRef: users[] is forbidden by assertCustomSecretRefUsersEmpty, so the desired list was structurally always empty and the chart wiped an org whose auth.ini fragment the operator advertised as authoritative. * System with users[] unset: the operator opted into OIDC but not into the chart-managed users-map, yet the Job still pruned every Main-Org member on their behalf. * CustomConfig-inline with users[] unset: same shape as System. The mode=None guard (from an earlier round) already had the same shape; extend the render gate to cover the other empty-list doors — now the Job renders only when `mode != None AND users non-empty`. The trade-off: taking `users:` back to `[]` no longer prunes the last entry — matching the already-documented `System → None` posture — called out explicitly in the docs. Signed-off-by: IvanHunters <[email protected]>
…p gate
Round-3 review from lllamnyp: any user in the shared `cozy` Keycloak
realm could sign into any tenant's Grafana. The audience mapper stops
replay of a minted token across releases, but does not gate who may
mint one — tenant-alice could complete the OAuth flow against
tenant-bob's Grafana. With `spec.oidc.users` non-empty the earlier
`allow_sign_up=false` fix mostly closed the door (though tenant-alice
still hit `error 401` after landing in Grafana, revealing the release
exists); with empty users under mode: System — a config the schema
permits and the docs describe as hands-off — the login succeeded and
minted a Viewer account for every cozy-realm identity.
The fix uses the KeycloakRealmGroups the tenant chart already
provisions (`<namespace>-{view,use,admin,super-admin}` in
packages/apps/tenant/templates/keycloakgroups.yaml) — no new
directory objects owned by the Monitoring release. Two chart edits:
1. oidc-keycloak.yaml: add the platform-managed realm-default `groups`
scope to `defaultClientScopes` explicitly. EDP's KeycloakClient
does NOT merge realm-level defaults into `defaultClientScopes`, so
the scope must be listed here or the `groups` claim would be
missing from the token.
2. grafana.yaml: in System mode, unconditionally emit
`allowed_groups: <ns>-view <ns>-use <ns>-admin <ns>-super-admin`
in `auth.generic_oauth` and add `groups` to `scopes`. Grafana
rejects any login whose token does not carry at least one of the
groups.
The gate is chart-owned and does not depend on the users-map — with
`users: []` tenant members still get a self-service Viewer through
`auto_assign_org_role`, cross-tenant users get 401 in Keycloak's OAuth
callback. Docs updated to reflect the two-layered contract
(tenant-membership gate + optional users-map).
Signed-off-by: IvanHunters <[email protected]>
… CEL claim gate Symmetric with the Grafana-side allowed_groups gate — round-3 review from lllamnyp on PR#3176 covered both surfaces, and #3044's original System-mode wiring shipped without this guard. The rationale is identical: audience binding stops REPLAY of a minted token across tenant kube-apiservers, but nothing stopped Keycloak from ISSUING a usable token to a caller outside the tenant. Absent a claim-side gate, any authenticated cozy-realm user could `kubectl oidc-login` against another tenant's cluster and land at `system:authenticated`. RBAC default-denies named-resource access, but `system:authenticated` still leaks discovery (kubectl api-resources, OpenAPI schemata) — tenant-alice could enumerate tenant-bob's cluster's CRD shape and built-in resource surface. Fix: add a CEL `claimValidationRules` entry to the rendered AuthenticationConfiguration (System mode only) that requires at least one of the release's four tenant-scoped Keycloak groups (`<namespace>-{view,use,admin,super-admin}`) to be present in the token's `groups` claim. The `has(claims.groups) &&` guard is required — CEL evaluation on a missing claim surfaces as HTTP 500 from the authenticator instead of a clean 401. CustomConfig mode is not touched — the tenant's own AuthenticationConfiguration is authoritative and the tenant is responsible for claim-side guards on their own issuer. Signed-off-by: IvanHunters <[email protected]>
…ctually works on Grafana v11.x Verified end-to-end on dev3 with Grafana v11.6.15: OAuth flow reaches Grafana, id_token and userinfo response both carry the correct `groups` claim (`[cozystack-cluster-admin, tenant-root-super-admin]` for kvaps), `allowed_groups` in grafana.ini lists `tenant-root-super-admin`, and login still fails with `user not a member of one of the required groups`. Grafana debug log shows `Attributes: map[]` — the extracted user's Groups slice is empty. On v11.x+ Grafana does not auto-map the `groups` claim onto user.Groups unless `groups_attribute_path` is set explicitly (the field is documented as optional but the gate is a silent no-op without it — a claim-name-string is not enough). Fix: hard-wire `groups_attribute_path=groups` in the chart-forced System-mode block alongside `allowed_groups`. "groups" points at the top-level `groups` array from userinfo, matching the shape Keycloak's oidc-group-membership-mapper emits (verified on the same dev3 run). Two helm-unittest cases updated to assert `groups_attribute_path` lives next to `allowed_groups` in both the users-map-active and empty-users branches (rejecting the silent no-op regression). Docs also updated to name the requirement. Signed-off-by: Ivan Okhotnikov <[email protected]>
… tenant apiserver boots The System-mode AuthenticationConfiguration rendered a claimValidationRule ranging over claims.groups, which is statically typed `any` in the apiserver CEL environment. The .exists() comprehension rejects a range of type any, so the tenant kube-apiserver failed config compilation at startup and CrashLooped — the control plane never booted and cluster teardown then hung the e2e cleanup budget. Wrap the range in dyn() so CEL treats it as dynamic; the has() guard still short-circuits when groups is absent and the gate semantics are unchanged. Strengthen the oidc unit test to pin dyn() (mutation-verified). Verified on a live tenant apiserver (v1.32). Signed-off-by: Ivan Okhotnikov <[email protected]>
0680946 to
5d13c15
Compare
Reconcile the guide with cozystack/cozystack#3176 as merged: - Drop the spec.oidc.grafanaAdmin paragraph: the field does not exist (oidc struct is mode/customConfig/users only) and server-level GrafanaAdmin promotion is out of scope for Phase 1; list it under What's out of scope instead. - Fix the mode-toggle note: the users-Job renders only when mode is not None and users[] is non-empty, so flipping to None prunes nothing and the provisioned accounts survive. - Clarify that removing a user is an org-membership removal, not deletion of the Grafana account. - Clarify that OIDC identifiers derive from the inner <CR-name>-system HelmRelease, so the example clientId tenant-acme-monitoring-system is correct. - Add ini language to the two config fenced blocks and replace Unicode ellipsis with ASCII. Signed-off-by: IvanHunters <[email protected]>
Reconcile the guide with cozystack/cozystack#3176 as merged: - Drop the spec.oidc.grafanaAdmin paragraph: the field does not exist (oidc struct is mode/customConfig/users only) and server-level GrafanaAdmin promotion is out of scope for Phase 1; list it under What's out of scope instead. - Fix the mode-toggle note: the users-Job renders only when mode is not None and users[] is non-empty, so flipping to None prunes nothing and the provisioned accounts survive. - Clarify that removing a user is an org-membership removal, not deletion of the Grafana account. - Clarify that OIDC identifiers derive from the inner <CR-name>-system HelmRelease, so the example clientId tenant-acme-monitoring-system is correct. - Add ini language to the two config fenced blocks and replace Unicode ellipsis with ASCII. Signed-off-by: IvanHunters <[email protected]>
Reconcile the guide with the merged chart (cozystack/cozystack#3176): - KeycloakRealmUser example: use spec.realmRef {name: keycloakrealm-cozy, kind: ClusterKeycloakRealm} instead of the non-existent spec.realm, and passwordSecret instead of the deprecated inline password. - CustomConfig inline: add enabled: "true" to the example and state that System wires it automatically while CustomConfig does not. - Correct the inline-merge direction: chart-forced settings win over the operator's map (Helm merge keeps the chart's values). - Fix the release-naming model: the OIDC-carrying release is monitoring-system in every namespace; instances differ by namespace, not release name. - Drop CEL claimValidationRules mentions: Grafana generic_oauth has no such option; enforce email_verified on the Keycloak side instead. - Note tls_client_ca is required for a self-signed BYO CA, and that spec.config["auth.generic_oauth"] is a literal ini-section key. Signed-off-by: IvanHunters <[email protected]>
## Summary Adds a new guide under `content/en/docs/next/operations/services/monitoring/`: **OIDC authentication for Grafana**. Documents how to enable per-user OIDC authentication on the Grafana instance shipped by every `Monitoring` release, using the new `spec.oidc` selector. Ships alongside [cozystack/cozystack#3176](cozystack/cozystack#3176), the code PR that adds the selector to the chart. The identity model is per-instance audience isolation on the flat `cozy` realm — same shape as the tenant kube-apiserver's Phase 1 ([cozystack/cozystack#3044](cozystack/cozystack#3044)); architectural rationale in [cozystack/community#24](cozystack/community#24). ## What - New page `content/en/docs/next/operations/services/monitoring/oidc-authentication.md` (`weight: 5`, positioned between `setup.md` (2) and `dashboards.md` (10)). - Covers all three modes (`None`, `System`, `CustomConfig`), the three role-mapping `KeycloakRealmGroup`s the chart owns, how a user is added via `KeycloakRealmUser`, browser login flow, and gotchas: `emailVerified` prescriptive requirement, self-signed CA under `CustomConfig.secretRef`, `admin_user` break-glass posture. - Explicit "out of scope" section (per-tenant realms, backend-logout-url, CEL `claimValidationRules`) so readers know where the boundary sits. - Only adds to `docs/next/`; version-specific pages (`v1.5`, ...) get the guide when the code PR lands in a release. ## Why - The `spec.oidc` selector on the `Monitoring` CR is a new user-facing surface — needs a user-facing guide before v1.6. - Uses the same tone and section shape as the tenant Kubernetes OIDC page (`content/en/docs/next/kubernetes/oidc-authentication.md`, #596), so operators land on a familiar layout. - Site builds cleanly in dev mode (Hugo, 1686+ pages, zero warnings); the page is reachable at `/docs/next/operations/services/monitoring/oidc-authentication/`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added a comprehensive OIDC authentication guide for Monitoring Grafana, including per-instance audience isolation to prevent token reuse across instances. * Documented OIDC modes (`None`, `System`, `CustomConfig`), required Grafana `auth.generic_oauth` settings, and group-based access control via namespace-scoped authorization with `groups_attribute_path`. * Explained `System` Keycloak artifact provisioning and automated user/role syncing, plus the forced Grafana behaviors when OIDC users are configured. * Detailed `CustomConfig` validation/precedence, key gotchas (e.g., `emailVerified`), CA/tls guidance, and how switching modes affects user reconciliation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
hack/e2e-apps/monitoring-oidc-system.bats and its customconfig twin have never executed. #2826 deleted the `test-apps-%:` rule that ran hack/e2e-apps/$*.bats along with every other file in that directory; #3176 landed three days later and added these two into it. Git raises no conflict when one branch empties a directory and another adds files to it, so the only signal was a directory name that still looked live. They could not pass as written either. hack/cozytest.sh defines no skip(), so the `skip` guarding the Keycloak assertions is a command-not-found and exit 127, and `kubectl api-resources --api-group=v1.edp.epam.com` exits 0 whether or not the group is served, so the guard was dead code in both directions. cac07db found and dropped both when it ported the kubernetes-oidc twins out of the same race. The render-side coverage is superseded, strictly, by packages/system/monitoring/tests/oidc_test.yaml -- 32 helm-unittest cases over the same Phase-1 selector, several of them asserting more than a live test can. `spec.public: false` is the example: the EDP CRD strips the field off the applied object, which the deleted bats said so itself. Four comments described hack/e2e-apps/ as a live directory; they now describe the shape rather than the instance, and two of them say what an e2e- prefix does not mean, since arming the cluster captures was never a claim that a runner exists. Signed-off-by: Myasnikov Daniil <[email protected]>
What this PR does
Adds a Phase-1 OIDC selector to the Grafana instance shipped by
packages/system/monitoring/, mirroring the shape of the tenant kube-apiserver PR (cozystack/cozystack#3044) so that operators can use one mental model for both. Rationale in cozystack/community#24 — per-cluster (here: per-Monitoring-release) audience isolation on the flatcozyrealm, no per-tenant realm.Authorization is app-side via a chart-owned
users:map reconciled by a post-install/post-upgrade Job into Grafana org membership. The chart does NOT own Keycloak directory objects (noKeycloakRealmGroups) and does NOT rely on Grafana'srole_attribute_path— same design shape as #3044's ClusterRoleBinding reconcile, adapted for Grafana orgs. Seedocs/oidc-grafana.mdfor the full rationale.The
MonitoringCR grows a new field:Modes
None(default) — no OIDC;admin_user/admin_passwordSecret is the only auth path. Existing releases render byte-identical.System— chart provisions, in the release namespace of the Monitoring CR:KeycloakClient(public: false, confidential,secretsourced from a chart-owned Secret via the EDP$<secret>:<key>reference;redirectUrislocked tohttps://grafana.<host>/login/generic_oauth).KeycloakClientScopewith anoidc-audience-mapperpinningid_token.audto the per-release clientId — the isolation primitive.client-secret(random on first install, preserved vialookup+fallback, same pattern aspackages/system/dashboard).spec.config.auth.generic_oauthsection wired to thecozyrealm issuer + per-release audience scope, with two chart-forced settings the users-Job depends on:skip_org_role_sync: "true"(login never overwrites the Job's org-role assignments) andoauth_allow_insecure_email_lookup: "true"(OIDC identity binds to the pre-provisioned local account by email).GF_AUTH_GENERIC_OAUTH_CLIENT_SECRETenv on the Grafana Deployment sourced from the Secret.spec.oidc.usersinto Grafana's Main Org.: pre-provisions a local account per listed email,POST /api/orgs/1/users+PATCH /api/orgs/1/users/{userId}to converge role changes, prunes every non-admin Main-Org member whose email is not in the list.GrafanaAdminpromotion (allow_assign_grafana_admin) is out of scope for Phase 1. Every Grafana instance (platform and tenant) caps at org-levelAdmin.CustomConfig— tenant supplies the whole[auth.generic_oauth]payload;cozyis not in the path. Two mutually exclusive paths:customConfig.config: {...}— inline map ofgrafana.inikeys. The chart merges the two chart-forced settings (skip_org_role_sync,oauth_allow_insecure_email_lookup) on top so the users-Job contract holds. The operator can NOT override those two (merge wins).customConfig.secretRef.name: ...— operator-owned Secret with anauth.inifragment; the chart mounts it under/etc/grafana/oidcand wiresGF_PATHS_CUSTOM_INI, and does NOT emitauth.generic_oauthunderspec.config(the ini fragment is authoritative). Because the chart cannot inject the two settings into a mounted ini,spec.oidc.usersis NOT supported in this branch — the chart hard-fails render on that combination.Fail-close guards
mode: Systemwithout_cluster.oidc-enabled == "true"(the platform-levelauthentication.oidc.enabledflag) → hard-fail with an actionable message pointing at the platform values.mode: Systemon a cluster where the EDP Keycloak operator CRDs (v1.edp.epam.com/v1) are not registered → hard-fail (symmetric onoidc-keycloak.yamlandgrafana.yaml).mode: CustomConfigwith both or neither ofconfig/secretRef.name→ hard-fail.mode: CustomConfigwithsecretRef.nameAND non-emptyusers→ hard-fail (chart cannot inject the two forced settings into an operator-mounted ini; forcing an explicit choice avoids silent contract loss).spec.oidc.users[]entry with missing / unknownroleor malformedemail→ hard-fail at render time so the users-Job never fires with a body that would 400 on Grafana's admin API.Break-glass posture
admin_user/admin_passwordanddisable_login_form: falseare unchanged in every mode. Locking the form off undermode: Systemis a documented follow-up hardening (seedocs/oidc-grafana.md).What's NOT in this PR (documented as out of scope)
backend-logout-urland matching Keycloak client attribute).claimValidationRulesgate onemail_verified— layered guarantees are described indocs/oidc-grafana.md.GrafanaAdminpromotion.Docs
docs/oidc-grafana.md— full operator guide (parallel todocs/oidc-tenant.md).docs/monitoring-oidc-authenticationbranch, undercontent/en/docs/next/operations/services/monitoring/oidc-authentication.md).Tests
helm-unittestcases intests/oidc_test.yaml+tests/oidc_crd_missing_test.yamlcover: all three modes; defensive nil paths (oidc: ~); System-mode Keycloak client + audience scope shape; CustomConfig chart-forced merge (positive assertion) ANDmode=CustomConfig inline — operator cannot override chart-forced OIDC settings(regression guard); CustomConfig secretRef +usersincompat fail-fast;spec.oidc.users[].rolemissing/unknown fail-fast; users-Job shape (hook annotations, RBAC-free, activeDeadlineSeconds,ttlSecondsAfterFinished: 3600, admin credential wiring); no chart-ownedKeycloakRealmGroups in any mode; norole_attribute_pathin Grafana config.hack/e2e-apps/monitoring-oidc-{system,customconfig}.bats— mirror the tenant kube-apiserver's e2e shape (no live browser flow — deferred to a follow-up integration suite, same as feat(apps/kubernetes): per-cluster OIDC selector for tenant kube-apiserver (Phase 1) #3044). Asserts on live cluster: users-Job appears withDESIRED_USERS_JSONpopulated, Grafana config carriesskip_org_role_sync=trueand norole_attribute_path, no chart-ownedKeycloakRealmGroups land incozy.Screenshots
N/A — no UI change on default values; the visual delta is Grafana's own "Sign in with Keycloak" button appearing under the login form when a release opts in.
Release note
Summary by CodeRabbit
None, platform-managedSystem, and tenant-controlledCustomConfigmodes.GrafanaAdmin.auth.ini(with mutual exclusivity validation).