Skip to content

fix(keycloak): keep KeycloakRealmUser roles when spec.roles is unset - #3531

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/keycloak-user-roles-nil-aware
Aug 14, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/keycloak-user-roles-nil-aware

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

The vendored keycloak-operator image is built from a pinned upstream commit with images/keycloak-operator/patches/*.diff applied at build time. This patch is a backport of epam/edp-keycloak-operator#372 onto that pin, so it is dropped in the same commit that moves COMMIT_REF past the upstream merge. The bug is epam/edp-keycloak-operator#371.

Under the default full strategy the operator treats spec.roles and spec.clientRoles[].roles as authoritative sets: after adding what is missing, it deletes every mapping the spec does not list. A resource that manages the user record and leaves those fields out therefore loses all of its realm roles on the first reconcile, default-roles-<realm> included, and with it offline_access, uma_authorization and the account client roles. The user keeps logging in and stops being able to do anything.

Upstream fixes this in two halves that only work together. The controller returns early when the desired list is nil, so an omitted field means "not managed" while an explicit roles: [] keeps its meaning of "remove every role". The json tags on Roles and ClientRoles[].Roles lose omitempty, because with it an explicit empty list is erased the first time the operator writes the object back, which adding its own finalizer already does, and comes back indistinguishable from an omitted field. Take that half away and the distinction holds only until the operator next touches the object.

All eight files from the upstream PR are here. Six apply to the pin unchanged. sync_user_roles.go and its test are rewritten against the pinned context, where the generated client package is still named keycloakv2 (upstream renamed it to keycloakapi after the pin, which is the only reason those two do not apply as-is). Tests match upstream: four controller cases for unset and empty realm and client roles, two in api/v1 for the serialization round trip.

Checked by extracting the pinned tarball, applying every .diff in the patches directory in the order the Dockerfile globs them, then go build ./... and go test ./internal/controller/keycloakrealmuser/... ./api/..., all green. Reverting each of the three conditions in turn fails exactly the tests that cover it, and nothing else.

One caveat a reader deserves up front: this patch does not reach any cluster yet. No CI path rebuilds this package, so hack/build-matrix.sh returns an empty matrix for a change confined to it, and values.yaml keeps pointing at an image built by hand in March. The same happened to 9c5fbb69a and 5b9b420ff, which edited this Dockerfile and left the pin alone, so the running binary is already older than the tree says. That gap is tracked in #3416 and is deliberately not addressed here: the fix for it is a choice between adding these packages to the build list and dropping the in-repo build, and the first option wakes a known mirror gap for split-host references. Landing the patch now means it is in place the moment that is resolved.

Screenshots

Not a UI change.

Downstream repositories

Walked the trigger map against the diff. It touches one build-time patch file under packages/system/keycloak-operator/images/, adds no package under packages/apps/ or packages/extra/, and changes no values key, schema, enum, default, CRD, ApplicationDefinition, namespace, label, annotation, metric, release asset, variant or node prerequisite. Nothing in the map fires.

Release note

fix(keycloak): a `KeycloakRealmUser` that omits `spec.roles` or leaves `roles` out of a `spec.clientRoles` entry no longer has the matching Keycloak role mappings deleted under the default `full` reconciliation strategy. Users keep `default-roles-<realm>` and the `offline_access`, `uma_authorization` and `account` roles it grants. Setting an explicit empty list still removes every role.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Keycloak role management for realm users and client roles.
    • Leaving a role list unspecified now preserves existing roles.
    • Explicitly providing an empty role list now removes all assigned roles.
  • Documentation
    • Updated API and resource documentation to clarify role-list behavior.
  • Tests
    • Added coverage for omitted, empty, managed, and unmanaged role configurations.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Role list semantics

Layer / File(s) Summary
Role serialization and API contracts
packages/system/keycloak-operator/images/keycloak-operator/patches/keycloakrealmuser-unset-roles.diff
Role fields retain explicit empty lists during serialization. CRD schemas, API documentation, and serialization tests define omitted, null, and empty-list behavior.
Role synchronization behavior
packages/system/keycloak-operator/images/keycloak-operator/patches/keycloakrealmuser-unset-roles.diff
Synchronization skips omitted realm and client roles. Explicit empty lists continue reconciliation and remove existing mappings. Controller tests cover both cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: ivanhunters, myasnikovdaniil, sircthulhu

Sequence Diagram(s)

sequenceDiagram
  participant KeycloakRealmUserSpec
  participant RoleSynchronization
  participant Keycloak
  KeycloakRealmUserSpec->>RoleSynchronization: provide realm and client role lists
  alt role list is omitted or null
    RoleSynchronization-->>Keycloak: skip role operations
  else role list is explicitly empty
    RoleSynchronization->>Keycloak: remove existing role mappings
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preserving roles when KeycloakRealmUser spec.roles is unset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/keycloak-user-roles-nil-aware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/keycloak Issues or PRs related to Keycloak (SSO / identity) kind/bug Categorizes issue or PR as related to a bug labels Aug 4, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/keycloak-user-roles-nil-aware branch from 7eb8f54 to 4b4ad55 Compare August 4, 2026 12:39
@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as ready for review August 5, 2026 07:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/keycloak-operator/images/keycloak-operator/patches/keycloakrealmuser-unset-roles.diff`:
- Around line 115-119: Update the packaged KeycloakRealmUser CRD descriptions
for both clientRoles and roles to document the omitted-versus-empty role-list
behavior, matching the descriptions in keycloakrealmuser-unset-roles.diff.
Regenerate the CRD if supported; otherwise patch the chart CRD directly while
preserving its YAML formatting.
🪄 Autofix

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 Plus

Run ID: 70a9f6d7-3ad3-4997-8833-122980aa4407

📥 Commits

Reviewing files that changed from the base of the PR and between 2f8c953 and 4b4ad55.

📒 Files selected for processing (1)
  • packages/system/keycloak-operator/images/keycloak-operator/patches/keycloakrealmuser-unset-roles.diff

Comment on lines +115 to +119
clientRoles:
- description: ClientRoles is a list of client roles assigned to user.
+ description: |-
+ ClientRoles is a list of client roles assigned to user.
+ The same rule as for Roles applies per client: omitting an entry's roles list leaves that client's roles untouched, an empty list removes all of them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'keycloakrealmuser|keycloak-operator/crds|keycloakrealmusers\.yaml|role' || true

echo
echo "Target diff lines:"
sed -n '1,220p' packages/system/keycloak-operator/images/keycloak-operator/patches/keycloakrealmuser-unset-roles.diff

echo
echo "Installed CRD excerpt around ClientRoles/Roles:"
if [ -f packages/system/keycloak-operator/charts/keycloak-operator/crds/v1.edp.epam.com_keycloakrealmusers.yaml ]; then
  rg -n -C 3 'clientRoles:|description:' packages/system/keycloak-operator/charts/keycloak-operator/crds/v1.edp.epam.com_keycloakrealmusers.yaml
fi

echo
echo "All generated/source CRDs with keycloakrealmusers names:"
git ls-files | rg 'keycloakrealmusers\.ya?ml$'

Repository: cozystack/cozystack

Length of output: 42051


Update the packaged KeycloakRealmUser CRD descriptions.

packages/system/keycloak-operator/charts/keycloak-operator/crds/v1.edp.epam.com_keycloakrealmusers.yaml still uses the old clientRoles and roles descriptions, so installed API docs do not expose the omitted-vs-empty role contract. Regenerate or patch this CRD.

🤖 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/keycloak-operator/images/keycloak-operator/patches/keycloakrealmuser-unset-roles.diff`
around lines 115 - 119, Update the packaged KeycloakRealmUser CRD descriptions
for both clientRoles and roles to document the omitted-versus-empty role-list
behavior, matching the descriptions in keycloakrealmuser-unset-roles.diff.
Regenerate the CRD if supported; otherwise patch the chart CRD directly while
preserving its YAML formatting.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/keycloak-user-roles-nil-aware branch from 4b4ad55 to d3286ae Compare August 9, 2026 22:15
The operator reconciles user roles as an authoritative set. Under the
default `full` strategy it deletes every realm role mapping missing
from spec.roles, and every client role mapping missing from the
matching spec.clientRoles entry. A consumer that manages only the user
record and leaves those fields out therefore has all of its roles
stripped on the first reconcile, `default-roles-<realm>` included,
which costs the user offline_access, uma_authorization and the account
client roles.

Treat an omitted list as "not managed" and leave Keycloak untouched
for that field, the same convention spec.identityProviders already
uses. An empty but non-nil list keeps its meaning, so revoking every
role is still possible with an explicit `roles: []`.

Drop omitempty from the two json tags as well. Without that an
explicit empty list is erased the first time the operator writes the
object back, adding its own finalizer is enough, and returns
indistinguishable from an omitted field, so the distinction does not
survive a round trip.

The patch mirrors epam/edp-keycloak-operator#372 file for file, so it
can be dropped in one move once COMMIT_REF moves past the upstream
merge.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patch applies clean against the pinned COMMIT_REF, the tree builds, the full upstream suite stays green, and all three mutations of the preservation logic turn the new tests red.

I ran git apply exactly as the Dockerfile does, against COMMIT_REF=facbc36 fetched from the same URL, exit 0. The two patch files touch disjoint file sets, so the shared atomic apply cannot conflict between them. CGO_ENABLED=0 go build ./cmd succeeds and go vet is clean on both patched packages.

Mutations. Dropping the realm guard reddens unset roles keep existing realm roles, and it fails for the right reason, mock: I don't know what to return because the method call was unexpected. GetUserRealmRoleMappings(...), which is the threat rather than a proxy for it. Dropping only the client-roles guard reddens exactly one test with no collateral, so the two guards are independently pinned. Restoring omitempty on both tags reddens the serialization test at both the realm level and nested in clientRoles[].

All three spellings of unset are covered, and I decoded real JSON through the patched types rather than reasoning about it. Absent key and explicit null both land on Roles == nil, so one guard covers both and the second needs no separate assertion. Empty list decodes non-nil with len 0 and means remove everything, covered by its own case. All three are accepted by the apiserver too, roles and clientRoles[].roles are nullable: true in the shipped CRD, so the "roles": null the operator will now emit is stored rather than rejected.

Two things worth writing down.

Nothing in cozystack CI runs these tests. The Dockerfile does go mod download and go build and never go test, the package Makefile has no test: target, and cozystack's own go-unit-tests covers a different module. So roughly 140 lines of the patch are carried for upstream's benefit and never execute here. The real regression guard is that git apply is atomic, so a COMMIT_REF bump that moves the hunks fails the build loudly. Worth a line in the patch header, so the next person to bump the ref knows which protection they have and which they do not.

The tag change touches three CRDs, not one. UserClientRole is shared by keycloakrealmuser_types.go:55, keycloakclient_types.go:213 and keycloakrealmgroup_types.go:57. It is safe, all three shipped CRDs mark that field nullable: true and the Go values are identical, only the wire form moves. But if any of them had not been nullable the failure would have been ugly: the group controller adds its finalizer, client.Update serializes "roles": null, the apiserver rejects it, and every KeycloakRealmGroup carrying a clientRoles entry wedges. One sentence in the commit message saves the next reader deriving that.

Same bug class survives for KeycloakRealmGroup, which has the identical field and no nil guard. Upstream scoped to users and this mirrors it faithfully, so not this PR's job, but worth an issue so it is not lost when the patch is dropped.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit d2551b6 into main Aug 14, 2026
16 of 18 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/keycloak-user-roles-nil-aware branch August 14, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/keycloak Issues or PRs related to Keycloak (SSO / identity) kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants