From 87da4011f70176f122e7473ffec3464cbe8bdf88 Mon Sep 17 00:00:00 2001 From: Daniel Fesenmeyer Date: Fri, 20 Sep 2024 20:06:08 +0200 Subject: [PATCH] Bugfix: "User Profile" attributes not available for Users Attribute search, when admin user does not have view- or manage-realm realm-management role (#31771) - UIRealmResource: add "info" sub-resource to get realm-related information, which is visible for ALL admins (users having any realm-management role); for now, only provide the information whether any user profile provider is enabled - UIRealmResourceTest: test the new endpoint, including permissions check - UserDataTable.tsx: use this resource to get the info whether user profile providers are enabled, instead of using the realm components resource (which requires "view-realm" permissions) - .../cypress/e2e/users_attribute_search_test.spec.ts: add cypress test to test the attribute search with minimum access rights - further small changes for reuse of components, test-code etc Closes #27536 Signed-off-by: Daniel Fesenmeyer --- .../cypress/e2e/client_scopes_test.spec.ts | 12 +- .../e2e/users_attribute_search_test.spec.ts | 100 +++++++++++ .../support/pages/admin-ui/ListingPage.ts | 11 +- .../pages/admin-ui/components/PageObject.ts | 80 ++++++--- .../manage/events/tabs/AdminEventsTab.ts | 20 ++- .../manage/events/tabs/UserEventsTab.ts | 8 + .../admin-ui/manage/users/UsersListingPage.ts | 88 ++++++++++ .../pages/admin-ui/manage/users/UsersPage.ts | 30 +++- .../cypress/support/util/AdminClient.ts | 14 +- .../src/components/users/UserDataTable.tsx | 28 ++-- .../UserDataTableAttributeSearchForm.tsx | 8 +- .../users/UserDataTableToolbarItems.tsx | 1 + .../src/context/auth/admin-ui-endpoint.ts | 7 + .../admin-ui/src/context/auth/uiRealmInfo.ts | 5 + .../src/user/details/SearchFilter.tsx | 1 + .../admin/ui/rest/UIRealmResource.java | 40 ++++- .../admin/ui/rest/model/UIRealmInfo.java | 16 ++ .../user/profile/UIRealmResourceTest.java | 156 ++++++++++++++++-- 18 files changed, 538 insertions(+), 87 deletions(-) create mode 100644 js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts create mode 100644 js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersListingPage.ts create mode 100644 js/apps/admin-ui/src/context/auth/uiRealmInfo.ts create mode 100644 rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/UIRealmInfo.java diff --git a/js/apps/admin-ui/cypress/e2e/client_scopes_test.spec.ts b/js/apps/admin-ui/cypress/e2e/client_scopes_test.spec.ts index f1ec9ab4e8..83c276fbca 100644 --- a/js/apps/admin-ui/cypress/e2e/client_scopes_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/client_scopes_test.spec.ts @@ -86,7 +86,7 @@ describe("Client Scopes test", () => { it("should filter items by Assigned type All types", () => { listingPage - .selectFilter(Filter.AssignedType) + .selectClientScopeFilter(Filter.AssignedType) .selectSecondaryFilterAssignedType(FilterAssignedType.AllTypes) .itemExist(FilterAssignedType.Default, true) .itemExist(FilterAssignedType.Optional, true) @@ -95,7 +95,7 @@ describe("Client Scopes test", () => { it("should filter items by Assigned type Default", () => { listingPage - .selectFilter(Filter.AssignedType) + .selectClientScopeFilter(Filter.AssignedType) .selectSecondaryFilterAssignedType(FilterAssignedType.Default) .itemExist(FilterAssignedType.Default, true) .itemExist(FilterAssignedType.Optional, false) @@ -104,7 +104,7 @@ describe("Client Scopes test", () => { it("should filter items by Assigned type Optional", () => { listingPage - .selectFilter(Filter.AssignedType) + .selectClientScopeFilter(Filter.AssignedType) .selectSecondaryFilterAssignedType(FilterAssignedType.Optional) .itemExist(FilterAssignedType.Default, false) .itemExist(FilterAssignedType.Optional, true) @@ -113,7 +113,7 @@ describe("Client Scopes test", () => { it("should filter items by Protocol All", () => { listingPage - .selectFilter(Filter.Protocol) + .selectClientScopeFilter(Filter.Protocol) .selectSecondaryFilterProtocol(FilterProtocol.All); sidebarPage.waitForPageLoad(); listingPage @@ -124,7 +124,7 @@ describe("Client Scopes test", () => { it("should filter items by Protocol SAML", () => { listingPage - .selectFilter(Filter.Protocol) + .selectClientScopeFilter(Filter.Protocol) .selectSecondaryFilterProtocol(FilterProtocol.SAML) .itemExist(FilterProtocol.SAML, true) .itemExist(openIDConnectItemText, false); //using FilterProtocol.OpenID will fail, text does not match. @@ -132,7 +132,7 @@ describe("Client Scopes test", () => { it("should filter items by Protocol OpenID", () => { listingPage - .selectFilter(Filter.Protocol) + .selectClientScopeFilter(Filter.Protocol) .selectSecondaryFilterProtocol(FilterProtocol.OpenID) .itemExist(FilterProtocol.SAML, false) .itemExist(openIDConnectItemText, true); //using FilterProtocol.OpenID will fail, text does not match. diff --git a/js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts b/js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts new file mode 100644 index 0000000000..1fd5bb133a --- /dev/null +++ b/js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts @@ -0,0 +1,100 @@ +import SidebarPage from "../support/pages/admin-ui/SidebarPage"; +import LoginPage from "../support/pages/LoginPage"; +import { keycloakBefore } from "../support/util/keycloak_hooks"; +import adminClient from "../support/util/AdminClient"; +import { + DefaultUserAttribute, + UserFilterType, +} from "../support/pages/admin-ui/manage/users/UsersListingPage"; +import UsersPage from "../support/pages/admin-ui/manage/users/UsersPage"; + +describe("Query by user attributes", () => { + const loginPage = new LoginPage(); + const sidebarPage = new SidebarPage(); + const usersPage = new UsersPage(); + const listingPage = usersPage.listing(); + + const emailSuffix = "@example.org"; + + const user1Username = "user-attrs-1"; + const user1FirstName = "John"; + const user1LastName = "Doe"; + const user1Pwd = "pwd"; + const user2Username = "user-attrs-2"; + const user2FirstName = "Jane"; + const user2LastName = user1LastName; + + before(async () => { + await cleanupTestData(); + const user1 = await adminClient.createUser({ + username: user1Username, + credentials: [ + { + type: "password", + value: user1Pwd, + }, + ], + email: user1Username + emailSuffix, + firstName: user1FirstName, + lastName: user1LastName, + enabled: true, + }); + const user1Id = user1.id!; + await adminClient.addClientRoleToUser(user1Id, "master-realm", [ + "view-users", + ]); + + await adminClient.createUser({ + username: user2Username, + email: user2Username + emailSuffix, + firstName: user2FirstName, + lastName: user2LastName, + enabled: true, + }); + }); + + beforeEach(() => { + loginPage.logIn(user1Username, user1Pwd); + keycloakBefore(); + sidebarPage.goToUsers(); + }); + + after(async () => { + await cleanupTestData(); + }); + + async function cleanupTestData() { + await adminClient.deleteUser(user1Username, true); + await adminClient.deleteUser(user2Username, true); + } + + it("Query with one attribute condition", () => { + listingPage + .selectUserSearchFilter(UserFilterType.AttributeSearch) + .openUserAttributesSearchForm() + .addUserAttributeSearchCriteria( + DefaultUserAttribute.lastName, + user1LastName, + ) + .triggerAttributesSearch() + .itemExist(user1Username, true) + .itemExist(user2Username, true); + }); + + it("Query with two attribute conditions", () => { + listingPage + .selectUserSearchFilter(UserFilterType.AttributeSearch) + .openUserAttributesSearchForm() + .addUserAttributeSearchCriteria( + DefaultUserAttribute.lastName, + user1LastName, + ) + .addUserAttributeSearchCriteria( + DefaultUserAttribute.firstName, + user1FirstName, + ) + .triggerAttributesSearch() + .itemExist(user1Username, true) + .itemExist(user2Username, false); + }); +}); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts index c67d2f871c..2b0d5ef229 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts @@ -52,7 +52,6 @@ export default class ListingPage extends CommonElements { public tableRowItem = "tbody tr[data-ouia-component-type]:visible"; #table = "table[aria-label]"; #filterSessionDropdownButton = ".pf-v5-c-select button:nth-child(1)"; - #searchTypeButton = "[data-testid='clientScopeSearch']"; #filterDropdownButton = "[data-testid='clientScopeSearchType']"; #protocolFilterDropdownButton = "[data-testid='clientScopeSearchProtocol']"; #kebabMenu = "[data-testid='kebab']"; @@ -320,9 +319,13 @@ export default class ListingPage extends CommonElements { return this; } - selectFilter(filter: Filter) { - cy.get(this.#searchTypeButton).click(); - cy.get(this.#dropdownItem).contains(filter).click(); + selectClientScopeFilter(filter: Filter) { + return this.selectFilter("clientScopeSearch", filter); + } + + protected selectFilter(searchTypeButtonTestId: string, filterStr: string) { + cy.get(`[data-testid='${searchTypeButtonTestId}']`).click(); + cy.get(this.#dropdownItem).contains(filterStr).click(); return this; } diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/components/PageObject.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/components/PageObject.ts index 7e218e7d2b..233d679919 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/components/PageObject.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/components/PageObject.ts @@ -8,13 +8,14 @@ export default class PageObject { #selectMenuToggleBtn = ".pf-v5-c-menu-toggle"; #switchInput = ".pf-v5-c-switch__input"; #formLabel = ".pf-v5-c-form__label"; - #chipGroup = ".pf-v5-c-chip-group"; #chipGroupCloseBtn = ".pf-v5-c-chip-group__close"; #chipItem = ".pf-v5-c-chip-group__list-item"; #emptyStateDiv = ".pf-v5-c-empty-state:visible"; #toolbarActionsButton = ".pf-v5-c-toolbar button[aria-label='Actions']"; #breadcrumbItem = ".pf-v5-c-breadcrumb .pf-v5-c-breadcrumb__item"; + genericChipGroupSelector = ".pf-v5-c-chip-group"; + protected assertExist(element: Cypress.Chainable, exist: boolean) { element.should((!exist ? "not." : "") + "exist"); return this; @@ -281,41 +282,55 @@ export default class PageObject { return this; } - #getChipGroup(groupName: string) { - return cy.get(this.#chipGroup).contains(groupName).parent().parent(); + #getChipGroup(groupSelector: string, groupName: string) { + return cy.get(groupSelector).contains(groupName).parent().parent(); } - #getChipItem(itemName: string) { - return cy.get(this.#chipItem).contains(itemName).parent(); + #getChipGroupWithLabel(groupSelector: string, label: string) { + cy.get(groupSelector) + .parent() + .find(".pf-v5-c-chip-group__label") + .contains(label); + + return cy.get(groupSelector); } - #getChipGroupItem(groupName: string, itemName: string) { - return this.#getChipGroup(groupName) + #getChipGroupItem( + groupSelector: string, + groupName: string, + itemName: string, + ) { + return this.#getChipGroup(groupSelector, groupName) .find(this.#chipItem) .contains(itemName) .parent(); } - protected removeChipGroup(groupName: string) { - this.#getChipGroup(groupName) + protected removeChipGroup(groupSelector: string, groupName: string) { + this.#getChipGroup(groupSelector, groupName) .find(this.#chipGroupCloseBtn) .find("button") .click(); return this; } - protected removeChipItem(itemName: string) { - this.#getChipItem(itemName).find("button").click(); + protected removeChipGroupItem( + groupSelector: string, + groupName: string, + itemName: string, + ) { + this.#getChipGroupItem(groupSelector, groupName, itemName) + .find("button") + .click(); return this; } - protected removeChipGroupItem(groupName: string, itemName: string) { - this.#getChipGroupItem(groupName, itemName).find("button").click(); - return this; - } - - protected assertChipGroupExist(groupName: string, exist: boolean) { - this.assertExist(cy.contains(this.#chipGroup, groupName), exist); + protected assertChipGroupExist( + groupSelector: string, + groupName: string, + exist: boolean, + ) { + this.assertExist(cy.contains(groupSelector, groupName), exist); return this; } @@ -325,20 +340,33 @@ export default class PageObject { return this; } - protected assertChipItemExist(itemName: string, exist: boolean) { - cy.get(this.#chipItem).within(() => { - cy.contains(itemName).should((exist ? "" : "not.") + "exist"); - }); - return this; - } - protected assertChipGroupItemExist( + groupSelector: string, groupName: string, itemName: string, exist: boolean, ) { this.assertExist( - this.#getChipGroup(groupName).contains(this.#chipItem, itemName), + this.#getChipGroup(groupSelector, groupName).contains( + this.#chipItem, + itemName, + ), + exist, + ); + return this; + } + + protected assertLabeledChipGroupItemExist( + groupSelector: string, + labelName: string, + itemName: string, + exist: boolean, + ) { + this.assertExist( + this.#getChipGroupWithLabel(groupSelector, labelName).contains( + this.#chipItem, + itemName, + ), exist, ); return this; diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/AdminEventsTab.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/AdminEventsTab.ts index d0d7af3d04..6d288bc08e 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/AdminEventsTab.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/AdminEventsTab.ts @@ -200,12 +200,16 @@ export default class AdminEventsTab extends PageObject { } public removeResourcePathChipGroup() { - super.removeChipGroup(AdminEventsTabSearchFormFieldsLabel.ResourcePath); + super.removeChipGroup( + this.genericChipGroupSelector, + AdminEventsTabSearchFormFieldsLabel.ResourcePath, + ); return this; } public assertResourceTypesChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.ResourceTypes, exist, ); @@ -214,6 +218,7 @@ export default class AdminEventsTab extends PageObject { public assertOperationTypesChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.OperationTypes, exist, ); @@ -222,6 +227,7 @@ export default class AdminEventsTab extends PageObject { public assertResourcePathChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.ResourcePath, exist, ); @@ -233,6 +239,7 @@ export default class AdminEventsTab extends PageObject { exist: boolean, ) { super.assertChipGroupItemExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.ResourcePath, itemName, exist, @@ -242,6 +249,7 @@ export default class AdminEventsTab extends PageObject { public assertRealmChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.Realm, exist, ); @@ -250,6 +258,7 @@ export default class AdminEventsTab extends PageObject { public assertClientChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.Client, exist, ); @@ -257,12 +266,17 @@ export default class AdminEventsTab extends PageObject { } public assertUserChipGroupExist(exist: boolean) { - super.assertChipGroupExist(AdminEventsTabSearchFormFieldsLabel.User, exist); + super.assertChipGroupExist( + this.genericChipGroupSelector, + AdminEventsTabSearchFormFieldsLabel.User, + exist, + ); return this; } public assertIpAddressChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.IpAddress, exist, ); @@ -271,6 +285,7 @@ export default class AdminEventsTab extends PageObject { public assertDateFromChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.DateFrom, exist, ); @@ -279,6 +294,7 @@ export default class AdminEventsTab extends PageObject { public assertDateToChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, AdminEventsTabSearchFormFieldsLabel.DateTo, exist, ); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/UserEventsTab.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/UserEventsTab.ts index 7e9154eaf7..c97070b90e 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/UserEventsTab.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/events/tabs/UserEventsTab.ts @@ -163,6 +163,7 @@ export default class UserEventsTab extends PageObject { public removeEventTypeChipGroupItem(itemName: string) { super.removeChipGroupItem( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.EventType, itemName, ); @@ -171,6 +172,7 @@ export default class UserEventsTab extends PageObject { public assertEventTypeChipGroupItemExist(itemName: string, exist: boolean) { super.assertChipGroupItemExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.EventType, itemName, exist, @@ -180,6 +182,7 @@ export default class UserEventsTab extends PageObject { public assertUserIdChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.UserId, exist, ); @@ -188,6 +191,7 @@ export default class UserEventsTab extends PageObject { public assertEventTypeChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.EventType, exist, ); @@ -196,6 +200,7 @@ export default class UserEventsTab extends PageObject { public assertClientChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.Client, exist, ); @@ -204,6 +209,7 @@ export default class UserEventsTab extends PageObject { public assertDateFromChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.DateFrom, exist, ); @@ -212,6 +218,7 @@ export default class UserEventsTab extends PageObject { public assertDateToChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.DateTo, exist, ); @@ -220,6 +227,7 @@ export default class UserEventsTab extends PageObject { public assertIpAddressChipGroupExist(exist: boolean) { super.assertChipGroupExist( + this.genericChipGroupSelector, UserEventsTabSearchFormFieldsLabel.IpAddress, exist, ); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersListingPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersListingPage.ts new file mode 100644 index 0000000000..c00186aa6d --- /dev/null +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersListingPage.ts @@ -0,0 +1,88 @@ +import ListingPage from "../../ListingPage"; +import UsersPage from "./UsersPage"; + +export enum DefaultUserAttribute { + username = "Username", + email = "Email", + firstName = "First name", + lastName = "Last name", +} + +export enum UserFilterType { + DefaultSearch = "Default search", + AttributeSearch = "Attribute search", +} + +export default class UsersListingPage extends ListingPage { + #dropdownPanelBtn = "[data-testid='dropdown-panel-btn']"; + #userAttributeSearchForm = "[data-testid='user-attribute-search-form']"; + #userAttributeSearchAddFilterBtn = + "[data-testid='user-attribute-search-add-filter-button']"; + #userAttributeSearchBtn = "[data-testid='search-user-attribute-btn']"; + #usersPage: UsersPage; + + constructor(usersPage: UsersPage) { + super(); + this.#usersPage = usersPage; + console.log("this.u1", usersPage); + console.log("this.u2", this.#usersPage); + } + + selectUserSearchFilter(filter: UserFilterType) { + super.selectFilter("user-search-toggle", filter); + + return this; + } + + openUserAttributesSearchForm() { + cy.get(this.#dropdownPanelBtn).click(); + cy.get(this.#userAttributeSearchForm).should("be.visible"); + + return this; + } + + addUserAttributeSearchCriteria( + defaultUserAttribute: DefaultUserAttribute, + attributeValue: string, + ) { + return this.addUserAttributeSearchCriteriaCustom( + defaultUserAttribute, + attributeValue, + ); + } + + addUserAttributeSearchCriteriaCustom( + attributeLabel: string, + attributeValue: string, + ) { + cy.get(this.#userAttributeSearchForm) + .find(".pf-m-typeahead") + .click() + .get(".pf-v5-c-menu__list-item") + .contains(attributeLabel) + .click({ force: true }); + + cy.get(this.#userAttributeSearchForm) + .find("#value") + .clear() + .type(attributeValue); + + cy.get(this.#userAttributeSearchForm) + .find(this.#userAttributeSearchAddFilterBtn) + .click(); + + this.#usersPage.assertAttributeSearchChipExists( + attributeLabel, + attributeValue, + true, + ); + + return this; + } + + triggerAttributesSearch() { + cy.get(this.#userAttributeSearchBtn).click(); + + return this; + } +} diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersPage.ts index 6907f6a31d..6fa691d6df 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/UsersPage.ts @@ -1,11 +1,12 @@ import PageObject from "../../components/PageObject"; -import ListingPage from "../../ListingPage"; - -const listingPage = new ListingPage(); +import UsersListingPage from "./UsersListingPage"; export default class UsersPage extends PageObject { #userListTabLink = "listTab"; #permissionsTabLink = "permissionsTab"; + #userAttributeSearchChipsGroup = + "[data-testid='user-attribute-search-chips-group']"; + #usersListingPage = new UsersListingPage(this); public goToUserListTab() { cy.findByTestId(this.#userListTabLink).click(); @@ -13,6 +14,10 @@ export default class UsersPage extends PageObject { return this; } + public listing() { + return this.#usersListingPage; + } + public goToPermissionsTab() { cy.findByTestId(this.#permissionsTabLink).click(); @@ -20,8 +25,23 @@ export default class UsersPage extends PageObject { } public goToUserDetailsPage(username: string) { - listingPage.searchItem(username); - listingPage.goToItemDetails(username); + this.#usersListingPage.searchItem(username); + this.#usersListingPage.goToItemDetails(username); + + return this; + } + + public assertAttributeSearchChipExists( + attributeLabel: string, + attributeValue: string, + exists: boolean, + ) { + super.assertLabeledChipGroupItemExist( + this.#userAttributeSearchChipsGroup, + attributeLabel, + attributeValue, + exists, + ); return this; } diff --git a/js/apps/admin-ui/cypress/support/util/AdminClient.ts b/js/apps/admin-ui/cypress/support/util/AdminClient.ts index 5cc9cafc30..67644f2da6 100644 --- a/js/apps/admin-ui/cypress/support/util/AdminClient.ts +++ b/js/apps/admin-ui/cypress/support/util/AdminClient.ts @@ -191,10 +191,18 @@ class AdminClient { }); } - async deleteUser(username: string) { + async deleteUser(username: string, ignoreNonExisting: boolean = false) { await this.#login(); - const user = await this.#client.users.find({ username }); - await this.#client.users.del({ id: user[0].id! }); + const foundUsers = await this.#client.users.find({ username }); + if (foundUsers.length == 0) { + if (ignoreNonExisting) { + return; + } else { + throw new Error(`User not found: ${username}`); + } + } + + await this.#client.users.del({ id: foundUsers[0].id! }); } async createClientScope(scope: ClientScopeRepresentation) { diff --git a/js/apps/admin-ui/src/components/users/UserDataTable.tsx b/js/apps/admin-ui/src/components/users/UserDataTable.tsx index b03cd9822f..2fc34489cc 100644 --- a/js/apps/admin-ui/src/components/users/UserDataTable.tsx +++ b/js/apps/admin-ui/src/components/users/UserDataTable.tsx @@ -1,8 +1,8 @@ -import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import { KeycloakDataTable, + KeycloakSpinner, ListEmptyState, useAlerts, useFetch, @@ -39,9 +39,10 @@ import { toAddUser } from "../../user/routes/AddUser"; import { toUser } from "../../user/routes/User"; import { emptyFormatter } from "../../util"; import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog"; -import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared"; import { BruteUser, findUsers } from "../role-mapping/resource"; import { UserDataTableToolbarItems } from "./UserDataTableToolbarItems"; +import { UiRealmInfo } from "../../context/auth/uiRealmInfo"; +import { fetchRealmInfo } from "../../context/auth/admin-ui-endpoint"; export type UserAttribute = { name: string; @@ -113,7 +114,7 @@ export function UserDataTable() { const { addAlert, addError } = useAlerts(); const { realm: realmName, realmRepresentation: realm } = useRealm(); const navigate = useNavigate(); - const [userStorage, setUserStorage] = useState(); + const [uiRealmInfo, setUiRealmInfo] = useState({}); const [searchUser, setSearchUser] = useState(""); const [selectedRows, setSelectedRows] = useState([]); const [searchType, setSearchType] = useState("default"); @@ -127,23 +128,17 @@ export function UserDataTable() { useFetch( async () => { - const testParams = { - type: "org.keycloak.storage.UserStorageProvider", - }; - try { return await Promise.all([ - adminClient.components.find(testParams), + fetchRealmInfo(adminClient), adminClient.users.getProfile(), ]); } catch { - return [[], {}] as [ComponentRepresentation[], UserProfileConfig]; + return [{}, {}] as [UiRealmInfo, UserProfileConfig]; } }, - ([storageProviders, profile]) => { - setUserStorage( - storageProviders.filter((p) => p.config?.enabled?.[0] === "true"), - ); + ([uiRealmInfo, profile]) => { + setUiRealmInfo(uiRealmInfo); setProfile(profile); }, [], @@ -171,7 +166,7 @@ export function UserDataTable() { ...params, }); } catch (error) { - if (userStorage?.length) { + if (uiRealmInfo.userProfileProvidersEnabled) { addError("noUsersFoundErrorStorage", error); } else { addError("noUsersFoundError", error); @@ -216,12 +211,12 @@ export function UserDataTable() { const goToCreate = () => navigate(toAddUser({ realm: realmName })); - if (!userStorage || !realm) { + if (!uiRealmInfo || !realm) { return ; } //should *only* list users when no user federation is configured - const listUsers = !(userStorage.length > 0); + const listUsers = !uiRealmInfo.userProfileProvidersEnabled; const clearAllFilters = () => { const filtered = [...activeFilters].filter( @@ -252,6 +247,7 @@ export function UserDataTable() { return ( setSelectAttributeKeyOpen(isOpen)} selections={getValues().displayName} @@ -188,7 +188,10 @@ export function UserDataTableAttributeSearchForm({ }; return ( -
+ {t("selectAttributes")} @@ -231,6 +234,7 @@ export function UserDataTableAttributeSearchForm({