Skip to content

Commit

Permalink
Bugfix: "User Profile" attributes not available for Users Attribute s…
Browse files Browse the repository at this point in the history
…earch, when admin user does not have view- or manage-realm realm-management role

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

Signed-off-by: Daniel Fesenmeyer <[email protected]>
  • Loading branch information
danielFesenmeyer committed Jul 31, 2024
1 parent adb2af4 commit 3f32fc9
Show file tree
Hide file tree
Showing 18 changed files with 536 additions and 85 deletions.
12 changes: 6 additions & 6 deletions js/apps/admin-ui/cypress/e2e/client_scopes_test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -124,15 +124,15 @@ 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.
});

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.
Expand Down
100 changes: 100 additions & 0 deletions js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
11 changes: 7 additions & 4 deletions js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']";
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JQuery>, exist: boolean) {
element.should((!exist ? "not." : "") + "exist");
return this;
Expand Down Expand Up @@ -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();
return this;
}

protected removeChipGroupItem(groupName: string, itemName: string) {
this.#getChipGroupItem(groupName, itemName).find("button").click();
protected removeChipGroupItem(
groupSelector: string,
groupName: string,
itemName: string,
) {
this.#getChipGroupItem(groupSelector, 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;
}

Expand All @@ -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");
});
protected assertChipGroupItemExist(
groupSelector: string,
groupName: string,
itemName: string,
exist: boolean,
) {
this.assertExist(
this.#getChipGroup(groupSelector, groupName).contains(
this.#chipItem,
itemName,
),
exist,
);
return this;
}

protected assertChipGroupItemExist(
groupName: string,
protected assertLabeledChipGroupItemExist(
groupSelector: string,
labelName: string,
itemName: string,
exist: boolean,
) {
this.assertExist(
this.#getChipGroup(groupName).contains(this.#chipItem, itemName),
this.#getChipGroupWithLabel(groupSelector, labelName).contains(
this.#chipItem,
itemName,
),
exist,
);
return this;
Expand Down
Loading

0 comments on commit 3f32fc9

Please sign in to comment.