IvanHunters 3fb8ec5b0e feat(monitoring): per-instance OIDC selector for the Grafana instance (Phase 1) (#3176)
<!-- Follow-up to cozystack/cozystack#3044 — same design family,
different subject. -->

## 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](https://github.com/cozystack/cozystack/pull/3044))
so that operators can use one mental model for both. Rationale in
[cozystack/community#24](https://github.com/cozystack/community/pull/24)
— per-cluster (here: per-Monitoring-release) audience isolation on the
flat `cozy` realm, 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 (no `KeycloakRealmGroup`s)
and does NOT rely on Grafana's `role_attribute_path` — same design shape
as #3044's ClusterRoleBinding reconcile, adapted for Grafana orgs. See
`docs/oidc-grafana.md` for the full rationale.

The `Monitoring` CR grows a new field:

```yaml
spec:
  oidc:
    mode: None | System | CustomConfig     # default None
    customConfig:                          # only for CustomConfig
      config: {}                           # inline grafana.ini keys, or...
      secretRef:
        name: ""                           # ...an operator-managed Secret
    users:                                 # optional; ignored in mode=None
      - email: alice@example.com
        role: Admin | Editor | Viewer
```

### Modes

- **`None`** (default) — no OIDC; `admin_user` / `admin_password` Secret
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, `secret` sourced from
a chart-owned Secret via the EDP `$<secret>:<key>` reference;
`redirectUris` locked to `https://grafana.<host>/login/generic_oauth`).
- `KeycloakClientScope` with an `oidc-audience-mapper` pinning
`id_token.aud` to the per-release clientId — the isolation primitive.
- Persistent Kubernetes Secret carrying the confidential `client-secret`
(random on first install, preserved via `lookup`+fallback, same pattern
as `packages/system/dashboard`).
- Grafana CR's `spec.config.auth.generic_oauth` section wired to the
`cozy` realm 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) and
`oauth_allow_insecure_email_lookup: "true"` (OIDC identity binds to the
pre-provisioned local account by email).
- `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET` env on the Grafana Deployment
sourced from the Secret.
- Post-install / post-upgrade **users-Job** reconciling
`spec.oidc.users` into 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.
- Server-level `GrafanaAdmin` promotion (`allow_assign_grafana_admin`)
is out of scope for Phase 1. Every Grafana instance (platform and
tenant) caps at org-level `Admin`.
- **`CustomConfig`** — tenant supplies the whole `[auth.generic_oauth]`
payload; `cozy` is not in the path. Two mutually exclusive paths:
- `customConfig.config: {...}` — inline map of `grafana.ini` keys. 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 an
`auth.ini` fragment; the chart mounts it under `/etc/grafana/oidc` and
wires `GF_PATHS_CUSTOM_INI`, and does NOT emit `auth.generic_oauth`
under `spec.config` (the ini fragment is authoritative). Because the
chart cannot inject the two settings into a mounted ini,
`spec.oidc.users` is NOT supported in this branch — the chart hard-fails
render on that combination.

### Fail-close guards

- `mode: System` without `_cluster.oidc-enabled == "true"` (the
platform-level `authentication.oidc.enabled` flag) → hard-fail with an
actionable message pointing at the platform values.
- `mode: System` on a cluster where the EDP Keycloak operator CRDs
(`v1.edp.epam.com/v1`) are not registered → hard-fail (symmetric on
`oidc-keycloak.yaml` and `grafana.yaml`).
- `mode: CustomConfig` with both or neither of `config` /
`secretRef.name` → hard-fail.
- `mode: CustomConfig` with `secretRef.name` AND non-empty `users` →
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 / unknown `role` or malformed
`email` → 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_password` and `disable_login_form: false` are
unchanged in every mode. Locking the form off under `mode: System` is a
documented follow-up hardening (see `docs/oidc-grafana.md`).

### What's NOT in this PR (documented as out of scope)

- Per-tenant Keycloak realms (Phase 2 territory, tracked in
community#24).
- Full-logout through Keycloak's end-session endpoint
(`backend-logout-url` and matching Keycloak client attribute).
- CEL `claimValidationRules` gate on `email_verified` — layered
guarantees are described in `docs/oidc-grafana.md`.
- Multi-issuer composition on one Monitoring release (mutually exclusive
modes).
- Server-level `GrafanaAdmin` promotion.

### Docs

- New `docs/oidc-grafana.md` — full operator guide (parallel to
`docs/oidc-tenant.md`).
- Website page follows in a paired PR against cozystack/website
(`docs/monitoring-oidc-authentication` branch, under
`content/en/docs/next/operations/services/monitoring/oidc-authentication.md`).

### Tests

- 33 `helm-unittest` cases in `tests/oidc_test.yaml` +
`tests/oidc_crd_missing_test.yaml` cover: all three modes; defensive nil
paths (`oidc: ~`); System-mode Keycloak client + audience scope shape;
CustomConfig chart-forced merge (positive assertion) AND
`mode=CustomConfig inline — operator cannot override chart-forced OIDC
settings` (regression guard); CustomConfig secretRef + `users` incompat
fail-fast; `spec.oidc.users[].role` missing/unknown fail-fast; users-Job
shape (hook annotations, RBAC-free, activeDeadlineSeconds,
`ttlSecondsAfterFinished: 3600`, admin credential wiring); no
chart-owned `KeycloakRealmGroup`s in any mode; no `role_attribute_path`
in Grafana config.
- Render-side bats in
`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 #3044). Asserts on live cluster:
users-Job appears with `DESIRED_USERS_JSON` populated, Grafana config
carries `skip_org_role_sync=true` and no `role_attribute_path`, no
chart-owned `KeycloakRealmGroup`s land in `cozy`.

### 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

```release-note
feat(monitoring): Each `Monitoring` CR can now opt its Grafana instance into OIDC via a flat selector — `spec.oidc.mode: System | CustomConfig | None` (default `None`). `System` trusts the platform `cozy` realm via a per-instance confidential Keycloak client + audience binding. Authorization is app-side: `spec.oidc.users: [{email, role: Admin|Editor|Viewer}]` is reconciled into Grafana's Main Org. by a chart-owned post-install/post-upgrade Job (pre-provision → org add → PATCH role → prune stale). `CustomConfig` accepts a tenant-supplied `[auth.generic_oauth]` payload — inline as a map (chart merges the two contract-critical settings `skip_org_role_sync` + `oauth_allow_insecure_email_lookup` on top) or as a Secret with an `auth.ini` key (in which case `spec.oidc.users` is not supported and the chart fails render on that combination). Server-level `GrafanaAdmin` promotion is out of scope for Phase 1. The `admin_user`/`admin_password` Secret stays a documented break-glass path in every mode.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added OIDC authentication for Monitoring Grafana with `None`,
platform-managed `System`, and tenant-controlled `CustomConfig` modes.
* System mode provisions platform-backed OIDC for each instance
(including persistent client credentials) and can optionally promote
matching users to server-level `GrafanaAdmin`.
* CustomConfig supports inline OAuth settings or Secret-backed
`auth.ini` (with mutual exclusivity validation).
2026-07-15 18:17:56 +03:00
2025-10-08 09:43:34 +05:00
2025-04-01 18:48:14 +02:00
2024-02-08 12:04:32 +01:00

Cozystack Cozystack

Open Source Apache-2.0 License Support Active GitHub Release GitHub Commit OpenSSF Best Practices OpenSSF Scorecard

Cozystack

Cozystack is a free platform and framework for building clouds.

Cozystack is a CNCF Sandbox Level Project that was originally built and sponsored by Ænix.

With Cozystack, you can transform a bunch of servers into an intelligent system with a simple REST API for spawning Kubernetes clusters, Database-as-a-Service, virtual machines, load balancers, HTTP caching services, and other services with ease.

Use Cozystack to build your own cloud or provide a cost-effective development environment.

Cozystack user interface

Use-Cases

Documentation

The documentation is located on the cozystack.io website.

Read the Getting Started section for a quick start.

If you encounter any difficulties, start with the troubleshooting guide and work your way through the process that we've outlined.

Versioning

Versioning adheres to the Semantic Versioning principles.
A full list of the available releases is available in the GitHub repository's Release section.

Contributions

Contributions are highly appreciated and very welcomed!

In case of bugs, please check if the issue has already been opened by checking the GitHub Issues section. If it isn't, you can open a new one. A detailed report will help us replicate it, assess it, and work on a fix.

You can express your intention to on the fix on your own. Commits are used to generate the changelog, and their author will be referenced in it.

If you have a concrete feature request, open an issue. For a cross-cutting or architectural change, open a design proposal in the community repo. For questions or early ideas, use GitHub Discussions. See CONTRIBUTING.md for the full routing.

Community

You are welcome to join our Telegram group and come to our weekly community meetings. Add them to your Google Calendar or iCal for convenience.

License

Cozystack is licensed under Apache 2.0.
The code is provided as-is with no warranties.

Commercial Support

A list of companies providing commercial support for this project can be found on official site.

Languages
Go 46.8%
Shell 37.9%
TypeScript 7%
Go Template 5.5%
Makefile 1.5%
Other 1.1%