Skip to content

Keep cache policy config multivalued on the custom provider screen - #53125

Open
dasniko wants to merge 1 commit into
keycloak:mainfrom
dasniko:pr/52029_federation-cache-policy-truncation
Open

dasniko wants to merge 1 commit into
keycloak:mainfrom
dasniko:pr/52029_federation-cache-policy-truncation

Conversation

@dasniko

@dasniko dasniko commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Closes #52029

What this fixes

SettingsCache addresses its config entries with an array index (config.maxLifespan[0]). That only resolves as long as the form holds the raw ComponentRepresentation, which is the case on the LDAP and Kerberos screens — both load with form.reset(component).

The custom provider screen loads with convertToFormValues(), which deliberately unwraps single-valued config entries into plain strings, because that is what DynamicComponents expects for the provider's own properties. The index then resolved against a string and yielded its first character.

Registering a control on an index path writes the resolved value back into the form, so the truncation happens on render and does not need an edit to be persisted:

stored displayed saved
maxLifespan: 50 5 ["5"]
eviction 12:30 01 / 03 ["1"] / ["3"]

shared/Header.tsx saves from the enable/disable toggle without a dirty check, so toggling a custom provider off and on is enough to lose the configured value.

This is option A as agreed in the issue: the multivalued shape of the five config entries that SettingsCache renders is restored after conversion, so the custom provider screen ends up with the same form shape as the LDAP and Kerberos screens. The provider's own properties stay unwrapped, and the existing save path accepts arrays unchanged.

A note on placement

The issue phrased option A as normalizing inside CustomProviderSettings.tsx. The helper is in SettingsCache.tsx instead, called from the custom screen, for two reasons:

  1. The key list mirrors the field names a few lines below it in the same file. Keeping both together means a cache field added later cannot be missed.
  2. It lets the regression test cover the conversion without pulling in the surrounding screen (admin client, contexts, router), which would otherwise have required mocking — something the contribution guidelines rule out.

Behaviour is identical either way; happy to inline it if you would rather keep the diff to a single file.

Testing

SettingsCache.test.tsx covers both load paths — the custom provider screen's convertToFormValues + setupCacheForm, and the form.reset(component) the LDAP and Kerberos screens use. Five assertions on the resulting form values, plus six that render the real controls and check the displayed value, an untouched save and an edited save, which is what catches the write-back.

Verified that 4 of the 11 fail without the fix, including both custom provider render assertions (expected '5' to be '50'), while every LDAP and Kerberos case stays green. Full admin-ui suite: 76/76. eslint, prettier and tsc are clean for the touched files.

No Playwright test: the e2e job starts a plain kc.sh start-dev with no deployed providers, and the custom provider screen only exists for an actually deployed UserStorageProvider, so covering it there would mean shipping a provider JAR with the e2e setup. Worth noting that the existing Kerberos cache policy specs only assert the policy label, never the value, which is why this went unnoticed.

Left out on purpose

NumberControl does value + 1 on a string, so stepping a stored 50 with the + button yields 501 — on every screen, LDAP included. That is an independent, pre-existing defect in a shared control used in around 20 places, so it is tracked separately in #53126 rather than fixed here.

Credit

#52198 by @MahathirMohammadShuvo addresses the same issue and he offered to defer this one. His analysis surfaced the render-time write-back and the missing dirty check in Header.tsx, both of which are reflected above, as well as the NumberControl defect. His variant makes SettingsCache itself shape-agnostic by deriving the field name from the shape present, which notably does not need the NumberControl special casing that option B was rejected for — worth a look if you would rather fix the shared component than the calling screen.

Generative AI disclosure

Claude Code was used to investigate the root cause and to draft the fix, the tests and this description. All changes were reviewed by me before submitting.

The cache fields address their config entries with an array index
(config.maxLifespan[0]), which only resolves as long as the form holds the
raw ComponentRepresentation, as it does on the LDAP and Kerberos screens.
The custom provider screen loads through convertToFormValues(), which
unwraps single valued entries into plain strings, so the index resolved
against a string and yielded its first character.

Registering a control on an index path writes the resolved value back into
the form, so the truncation happened on render and did not need an edit to
be persisted: a stored max lifespan of 50 was shown as 5 and saved as 5, an
eviction time of 12:30 as 01:03. Header.tsx saves from the enable/disable
toggle without a dirty check, so toggling a custom provider off and on was
enough to lose the configured value.

Restore the multivalued shape of those entries after conversion. The helper
is placed next to the fields whose names it mirrors, so that a cache field
added later cannot be missed, and so that the regression test can cover the
conversion without pulling in the surrounding screen.

Closes keycloak#52029

Signed-off-by: Niko Köbler <niko@n-k.de>
@dasniko
dasniko marked this pull request as ready for review September 23, 2026 16:26
@dasniko
dasniko requested review from a team as code owners September 23, 2026 16:26
Copilot AI balanced review requested due to automatic review settings September 23, 2026 16:26

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

All reviewed changes are covered by regression tests, with no unresolved issues.

Review effort: Balanced
Findings: None

What changed in this PR

Fixes truncated cache-policy values on custom provider forms by restoring array-shaped configuration values before rendering.

Changes:

  • Normalizes five cache configuration fields.
  • Applies normalization when loading custom providers.
  • Adds regression tests for custom, LDAP, and Kerberos paths.
File Description
js/​apps/​admin-ui/​src/​user-federation/​shared/​SettingsCache.tsx Adds cache form-shape normalization.
js/​apps/​admin-ui/​src/​user-federation/​shared/​SettingsCache.test.tsx Tests loading, rendering, and saving cache values.
js/​apps/​admin-ui/​src/​user-federation/​custom/​CustomProviderSettings.tsx Applies normalization after form conversion.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin Console: Cache policy values are truncated for custom User Federation providers

2 participants