8000 added optional storage type for the declarative ui by edewit · Pull Request #47859 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

added optional storage type for the declarative ui - #47859

Open
edewit wants to merge 26 commits into
keycloak:mainfrom
edewit:storage-type-declarative-ui
Open

edewit wants to merge 26 commits into
keycloak:mainfrom
edewit:storage-type-declarative-ui

Conversation

@edewit
@edewit edewit commented Apr 8, 2026
Copy link
Copy Markdown
Contributor

By adding a storage type to your UI it can save the config onto other
models and bypass the update and create logic from your provider

fixes: #47858

Signed-off-by: Erik Jan de Wit erikjan.dewit@gmail.com

@edewit
edewit requested review from a team as code owners April 8, 2026 15:08
@edewit
edewit force-pushed the storage-type-declarative-ui branch from e69d211 to fabcd85 Compare June 29, 2026 09:17
Copilot AI balanced review requested due to automatic review settings June 29, 2026 09:17
@edewit
edewit commented Jun 29, 2026
Copy link
Copy Markdown
Contributor Author

@ssilvert would this be something we could add?

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.

Pull request overview

This PR extends the declarative Admin UI extension mechanism to support alternative persistence backends beyond Keycloak Components by introducing a storageType (and optional custom endpoint) in provider metadata, and updating the Admin UI to load/save configuration accordingly.

Changes:

  • Add StorageType and expose storageType (and optional endpoint) via UiTabProviderFactory#getTypeMetadata().
  • Update Admin UI PageHandler to fetch and persist tab/page config to different entity types (client/user/group/IdP) or to a custom REST endpoint.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
server-spi-private/src/main/java/org/keycloak/services/ui/extend/UiTabProviderFactory.java Exposes storageType and optional endpoint in UI extension metadata; adds new defaults and Javadoc.
server-spi-private/src/main/java/org/keycloak/services/ui/extend/StorageType.java Introduces a new enum describing supported storage backends for UI tab data.
js/apps/admin-ui/src/page/PageHandler.tsx Adds storage-type-specific fetch/save logic, including custom endpoint GET/PUT support.

Copilot AI review requested due to automatic review settings June 29, 2026 10:07
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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

@edewit
edewit commented Jul 10, 2026
Copy link
Copy Markdown
Contributor Author

@ssilvert can you have a look?

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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings July 10, 2026 08:28
@edewit
edewit force-pushed the storage-type-declarative-ui branch from 17fe945 to 2054da4 Compare July 10, 2026 08:28
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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

@ssilvert
ssilvert commented Jul 10, 2026 •
Copy link
Copy Markdown
Contributor

@edewit I'm going to wait until the Copilot comments are resolved one way or another before I do a review. Just let me know when you are ready.

@edewit
edewit commented Jul 13, 2026
Copy link
Copy Markdown
Contributor Author

@ssilvert All Copilot review threads have been resolved.

Summary of changes addressing the valid feedback:

  • Route params: Added resolveTabParams() / getEntityId() in pageHandlerStorage.ts to match tab metadata.path against the current URL. USER/GROUP now use id, IDENTITY_PROVIDER uses alias, with getLastId() fallback for nested group paths.
  • Config serialization: Added normalizeConfig() — multivalued strings serialize with ## for string-map targets (CLIENT, IDENTITY_PROVIDER); list-map targets (USER, GROUP) wrap scalars in arrays.
  • Custom endpoints: interpolateEndpoint() now URL-encodes values and throws when placeholders are missing.
  • Java docs: Updated StorageType / UiTabProviderFactory Javadoc to document actual route param names (id, alias).

Previously reported issues (REALM javadoc, useForm typing, syntax errors, validation placement) were already fixed in earlier commits.

Ready for your review when you have a chance.

Copilot AI review requested due to automatic review settings July 13, 2026 11:31
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.

🔵 Needs a closer look

Three unresolved moderate findings affect component discovery and permission consistency.

Review details

Suppressed comments (3)

services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java:129

  • When type is queried without a providerId, this branch selects only customComponents as soon as any custom-storage factory exists. That silently drops all realm-backed components of the same type whenever custom and default providers are installed together; this path should fall through to the merge below so both backends are returned.
        } else if (customComponents != null && type != null && providerId == null
                && !UiExtensionComponentStorage.getCustomStorageProviderIds(session, type).isEmpty()) {
            components = customComponents;

services/src/main/java/org/keycloak/services/resources/admin/UiExtensionComponentStorage.java:59

  • With neither type nor providerId, this returns null, causing ComponentResource to use only realm.getComponentsStream() and omit every component held by a ComponentStorageFactory. Consequently custom-storage components are not discoverable through the standard unfiltered GET /components endpoint, unlike realm-backed components; enumerate the applicable custom UI storage factories for this query as well.
        if (type == null) {
            return null;

services/src/main/java/org/keycloak/services/resources/admin/UiExtensionPermissions.java:20

  • The UI treats an empty requiredViewRoles or requiredManageRoles metadata list as the default realm role, and requireManage does the same, but requireView calls hasAnyRole directly on empty lists. A provider overriding either method with List.of() can therefore be displayed by the UI while its view/config/component requests are rejected with 403; apply the same default-role fallback here.
    static void requireView(AdminPermissionEvaluator auth, UiExtensionSupport extension) {
        if (hasAnyRole(auth, extension.getRequiredViewRoles())
                || hasAnyRole(auth, extension.getRequiredManageRoles())) {
            return;
        }
        throw new ForbiddenException();
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

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.

(data) => {
form.reset({});
if (data) {
convertToFormValues(data, form.setValue);
Comment on lines +39 to +41
default List<ProviderConfigProperty> getConfigProperties(KeycloakSession session, Map<String, String> contextParams) {
if (this instanceof ConfiguredProvider configuredProvider) {
return configuredProvider.getConfigProperties();
Comment on lines +136 to +139
} else if (customComponents != null && type != null && providerId == null
&& !UiExtensionComponentStorage.getCustomStorageProviderIds(session, type).isEmpty()) {
components = customComponents;
} else if (customComponents != null) {
Comment on lines +58 to +59
if (type == null) {
return null;
Comment on lines +16 to +17
if (hasAnyRole(auth, extension.getRequiredViewRoles())
|| hasAnyRole(auth, extension.getRequiredManageRoles())) {
edewit added 26 commits November 3, 2026 11:03
By adding a storage type to your UI it can save the config onto other
models and bypass the update and create logic from your provider

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Scope normalizeConfig to declared properties only, document the CUSTOM
endpoint protocol, and add unit tests for pageHandlerStorage helpers.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Treat IdentityProviderMultiList as multivalued for string-map round-trips
and delete declared properties when form controls clear their values.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Guard against missing client, user, and identity provider resources
before narrowing types used by mergeEntityConfig.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Add runtime config endpoint, FGAP-aware permissions, navigation sections,
tab i18n, page detail tabs, ComponentStorageFactory SPI, admin guide,
and promote DECLARATIVE_UI to preview.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Fix permission checks, runtime config sync, dotted property form mapping,
custom component storage lookup, realm URL encoding, and route access for
extension pages with custom roles.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Pass all runtime config query params, gate page mutations by manage roles,
show extension nav when only extension pages are visible, filter internal
custom storage factories, and treat empty multiline values as cleared.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Revalidate component provider after update, block read-only form submits,
document custom endpoint authorization, clarify detailTabPath contract,
and use detailTabPath in page list links.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Introduce a test UiPageProviderFactory with custom in-memory storage
and verify ComponentResource CRUD delegates outside the realm component
table when ComponentStorageFactory is implemented.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Aggregate custom-storage components in type-only queries, reject provider
changes across custom-storage boundaries, and skip page fetches for users
without view access.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Check realm permissions before returning 404 for missing components,
return internal components for update/delete so forbidden is returned
instead of not found, validate custom-storage provider changes before
model update, and resolve custom storage factories by provider ID alone.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Reset tab component IDs on route changes, avoid findOne for tab-backed
loads, filter type-wide component queries per factory permissions, and
treat manage roles as sufficient for view access in the declarative UI.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Exclude realm component entries for providers that delegate to
ComponentStorageFactory when merging with custom-storage results so
type-only component queries do not double-count the same entries.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Merge realm and custom-storage component streams by ID so UI extension
components are not listed twice when both stores expose the same entry.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
When a provider type is backed only by custom storage factories, return
the custom component stream directly and never persist those components
to the realm component table on create.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Filter realm components by custom-storage provider IDs and deduplicate
custom-storage listings by component ID so type-wide queries return a
single entry for custom-storage UI page components.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
List custom-storage components without merging realm entries when the
provider type is fully custom-backed, and clean up UiPageProvider
components between test methods to avoid static storage leakage.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
The previous commit referenced this method in @BeforeEach/@AfterEach but
never added the implementation, which broke the build.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Replace normalizeConfig with pickDeclaredConfig, convertToFormValues, stringify on DynamicComponents, and a slimmer mergeEntityConfig.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
findOne can return null, which is incompatible with useState's setter
type and broke the admin-ui TypeScript build.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
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.

🔵 Needs a closer look

Unresolved defects omit stored components from queries, silently lose runtime-defined fields, break multivalued inputs, and incorrectly deny realm-admin reads.

5 open issues

Review effort: Balanced

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.

🟡 Changes recommended

Unresolved defects omit components from listings, prevent runtime-defined fields from loading and saving, and break form restoration, multivalued inputs, and permission fallbacks.

6 open issues

Review effort: Balanced

Comment on lines +259 to +262
form.reset({});
if (data) {
convertToFormValues(data, form.setValue);
}
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.

Change the custom UI so it can save different types, not just components

4 participants

0