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 <daniel.fesenmeyer@bosch.com>
This commit is contained in:
parent
c532751ff4
commit
87da4011f7
18 changed files with 538 additions and 87 deletions
|
@ -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.
|
||||
|
|
100
js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts
Normal file
100
js/apps/admin-ui/cypress/e2e/users_attribute_search_test.spec.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<ComponentRepresentation[]>();
|
||||
const [uiRealmInfo, setUiRealmInfo] = useState<UiRealmInfo>({});
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
const [searchType, setSearchType] = useState<SearchType>("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 <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
//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 (
|
||||
<ChipGroup
|
||||
className="pf-v5-u-mt-md pf-v5-u-mr-md"
|
||||
data-testid="user-attribute-search-chips-group"
|
||||
key={entry.name}
|
||||
categoryName={
|
||||
entry.displayName.length ? entry.displayName : entry.name
|
||||
|
|
|
@ -135,7 +135,7 @@ export function UserDataTableAttributeSearchForm({
|
|||
if (profile) {
|
||||
return (
|
||||
<KeycloakSelect
|
||||
data-testid="search-attribute-name"
|
||||
data-testid="search-attribute-name-select"
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={(isOpen) => setSelectAttributeKeyOpen(isOpen)}
|
||||
selections={getValues().displayName}
|
||||
|
@ -188,7 +188,10 @@ export function UserDataTableAttributeSearchForm({
|
|||
};
|
||||
|
||||
return (
|
||||
<Form className="user-attribute-search-form">
|
||||
<Form
|
||||
className="user-attribute-search-form"
|
||||
data-testid="user-attribute-search-form"
|
||||
>
|
||||
<TextContent className="user-attribute-search-form-headline">
|
||||
<Text component={TextVariants.h2}>{t("selectAttributes")}</Text>
|
||||
</TextContent>
|
||||
|
@ -231,6 +234,7 @@ export function UserDataTableAttributeSearchForm({
|
|||
</InputGroupItem>
|
||||
<InputGroupItem>
|
||||
<Button
|
||||
data-testid="user-attribute-search-add-filter-button"
|
||||
variant="control"
|
||||
icon={<CheckIcon />}
|
||||
onClick={addToFilter}
|
||||
|
|
|
@ -126,6 +126,7 @@ export function UserDataTableToolbarItems({
|
|||
return (
|
||||
<>
|
||||
<DropdownPanel
|
||||
data-testid="select-attributes-dropdown"
|
||||
buttonText={t("selectAttributes")}
|
||||
setSearchDropdownOpen={setSearchDropdownOpen}
|
||||
searchDropdownOpen={searchDropdownOpen}
|
||||
|
|
|
@ -3,6 +3,7 @@ import KeycloakAdminClient, {
|
|||
} from "@keycloak/keycloak-admin-client";
|
||||
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
|
||||
import { joinPath } from "../../utils/joinPath";
|
||||
import { UiRealmInfo } from "./uiRealmInfo";
|
||||
|
||||
export async function fetchAdminUI<T>(
|
||||
adminClient: KeycloakAdminClient,
|
||||
|
@ -27,3 +28,9 @@ export async function fetchAdminUI<T>(
|
|||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function fetchRealmInfo(
|
||||
adminClient: KeycloakAdminClient,
|
||||
): Promise<UiRealmInfo> {
|
||||
return fetchAdminUI(adminClient, `ui-ext/info`);
|
||||
}
|
||||
|
|
5
js/apps/admin-ui/src/context/auth/uiRealmInfo.ts
Normal file
5
js/apps/admin-ui/src/context/auth/uiRealmInfo.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
/** Information about a realm, which is available for all admins of the realm */
|
||||
export interface UiRealmInfo {
|
||||
/** Whether at least one user storage provider is enabled */
|
||||
userProfileProvidersEnabled?: boolean;
|
||||
}
|
|
@ -46,6 +46,7 @@ export const SearchDropdown = ({
|
|||
onOpenChange={(isOpen) => setSearchToggle(isOpen)}
|
||||
toggle={(ref) => (
|
||||
<MenuToggle
|
||||
data-testid="user-search-toggle"
|
||||
ref={ref}
|
||||
id="toggle-id"
|
||||
onClick={() => setSearchToggle(!searchToggle)}
|
||||
|
|
|
@ -19,20 +19,30 @@
|
|||
|
||||
package org.keycloak.admin.ui.rest;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.InternalServerErrorException;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status.Family;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.keycloak.admin.ui.rest.model.UIRealmRepresentation;
|
||||
import org.keycloak.admin.ui.rest.model.UIRealmInfo;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.StorageProviderRealmModel;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||
import org.keycloak.services.resources.admin.RealmAdminResource;
|
||||
import org.keycloak.services.resources.admin.UserProfileResource;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.InternalServerErrorException;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status.Family;
|
||||
|
||||
/**
|
||||
* This JAX-RS resource is decorating the Admin Realm API in order to support specific behaviors from the
|
||||
|
@ -67,6 +77,24 @@ public class UIRealmResource {
|
|||
return response;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("info")
|
||||
@Operation(summary = "Gets information about the realm, viewable by all realm admins")
|
||||
@APIResponse(responseCode = "200", description = "", content = {
|
||||
@Content(schema = @Schema(implementation = UIRealmInfo.class, type = SchemaType.OBJECT))})
|
||||
public UIRealmInfo getInfo() {
|
||||
auth.requireAnyAdminRole();
|
||||
|
||||
final var info = new UIRealmInfo();
|
||||
info.setUserProfileProvidersEnabled(isAtLeastOneUserStorageProviderEnabled());
|
||||
return info;
|
||||
}
|
||||
|
||||
private boolean isAtLeastOneUserStorageProviderEnabled() {
|
||||
return ((StorageProviderRealmModel) session.getContext().getRealm()).getUserStorageProvidersStream()
|
||||
.anyMatch(UserStorageProviderModel::isEnabled);
|
||||
}
|
||||
|
||||
private void updateUserProfileConfiguration(UIRealmRepresentation rep) {
|
||||
UPConfig upConfig = rep.getUpConfig();
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package org.keycloak.admin.ui.rest.model;
|
||||
|
||||
/**
|
||||
* Information about a realm, which is available for each admin of a realm, not only for admins allowed to view the realm.
|
||||
*/
|
||||
public class UIRealmInfo {
|
||||
private boolean userProfileProvidersEnabled;
|
||||
|
||||
public boolean isUserProfileProvidersEnabled() {
|
||||
return userProfileProvidersEnabled;
|
||||
}
|
||||
|
||||
public void setUserProfileProvidersEnabled(final boolean userProfileProvidersEnabled) {
|
||||
this.userProfileProvidersEnabled = userProfileProvidersEnabled;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.user.profile;
|
|||
|
||||
import jakarta.ws.rs.client.Client;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.client.WebTarget;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
|
@ -27,36 +28,100 @@ import java.io.IOException;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.KeycloakBuilder;
|
||||
import org.keycloak.admin.client.resource.BearerAuthFilter;
|
||||
import org.keycloak.admin.client.token.TokenManager;
|
||||
import org.keycloak.admin.ui.rest.model.UIRealmRepresentation;
|
||||
import org.keycloak.admin.ui.rest.model.UIRealmInfo;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributeRequired;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.util.AssertAdminEvents;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class UIRealmResourceTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
private static final String TEST_PWD = "password";
|
||||
private static final String USER_WITH_VIEW_USERS_ROLE = "user-with-view-users-role";
|
||||
private static final String USER_WITHOUT_ADMIN_ROLE = "user-without-admin-role";
|
||||
|
||||
private static Client httpClient;
|
||||
private static Keycloak keycloakAdminClientViewUsers;
|
||||
private static Keycloak keycloakAdminClientWithoutAdminRoles;
|
||||
|
||||
@Rule
|
||||
public AssertAdminEvents assertAdminEvents = new AssertAdminEvents(this);
|
||||
|
||||
@BeforeClass
|
||||
public static void initHttpClients() {
|
||||
httpClient = Keycloak.getClientProvider().newRestEasyClient(null, null, true);
|
||||
|
||||
keycloakAdminClientViewUsers = KeycloakBuilder.builder().serverUrl(getKeycloakServerUrl())
|
||||
.realm(TEST_REALM_NAME)
|
||||
.username(USER_WITH_VIEW_USERS_ROLE)
|
||||
.password(TEST_PWD)
|
||||
.clientId(Constants.ADMIN_CLI_CLIENT_ID)
|
||||
.resteasyClient(httpClient)
|
||||
.build();
|
||||
|
||||
keycloakAdminClientWithoutAdminRoles = KeycloakBuilder.builder().serverUrl(getKeycloakServerUrl())
|
||||
.realm(TEST_REALM_NAME)
|
||||
.username(USER_WITHOUT_ADMIN_ROLE)
|
||||
.password(TEST_PWD)
|
||||
.clientId(Constants.ADMIN_CLI_CLIENT_ID)
|
||||
.resteasyClient(httpClient)
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeHttpClients() {
|
||||
if (keycloakAdminClientViewUsers != null) {
|
||||
keycloakAdminClientViewUsers.close();
|
||||
}
|
||||
if (keycloakAdminClientWithoutAdminRoles != null) {
|
||||
keycloakAdminClientWithoutAdminRoles.close();
|
||||
}
|
||||
if (httpClient != null) {
|
||||
httpClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
public void configureTestRealm(final RealmRepresentation testRealm) {
|
||||
final var userWithViewUsersRole = createTestUserRep(USER_WITH_VIEW_USERS_ROLE,
|
||||
Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.VIEW_USERS);
|
||||
testRealm.getUsers().add(userWithViewUsersRole);
|
||||
|
||||
final var userWithoutAdminRole = createTestUserRep(USER_WITHOUT_ADMIN_ROLE,
|
||||
Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, AccountRoles.VIEW_GROUPS);
|
||||
testRealm.getUsers().add(userWithoutAdminRole);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -89,55 +154,112 @@ public class UIRealmResourceTest extends AbstractTestRealmKeycloakTest {
|
|||
AdminEventRepresentation adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
|
||||
Assert.assertNotNull(adminEvent.getRepresentation());
|
||||
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
|
||||
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
|
||||
upConfig.getAttribute("foo").setDisplayName("Foo");
|
||||
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
|
||||
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
|
||||
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
|
||||
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
|
||||
upConfig.getAttribute("foo").setPermissions(new UPAttributePermissions(Set.of(), Set.of(UPConfigUtils.ROLE_USER)));
|
||||
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
|
||||
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
|
||||
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
|
||||
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
|
||||
upConfig.getAttribute("foo").setRequired(new UPAttributeRequired(Set.of(UPConfigUtils.ROLE_ADMIN, UPConfigUtils.ROLE_USER), Set.of()));
|
||||
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
|
||||
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
|
||||
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
|
||||
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
|
||||
upConfig.getAttribute("foo").setValidations(Map.of("length", Map.of("min", "3", "max", "128")));
|
||||
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
|
||||
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
|
||||
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
|
||||
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
|
||||
|
||||
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
|
||||
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
|
||||
assertAdminEvents.assertEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uiRealmInfoSucceedsWithAnyAdminRole() {
|
||||
final var response = getUiRealmInfo(keycloakAdminClientViewUsers.tokenManager());
|
||||
|
||||
assertEquals(Status.OK.getStatusCode(), response.getStatus());
|
||||
final var responseStr = response.readEntity(String.class);
|
||||
final var uiRealmInfo = toUiRealmInfo(responseStr);
|
||||
assertNotNull(uiRealmInfo);
|
||||
assertFalse(uiRealmInfo.isUserProfileProvidersEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uiRealmInfoFailsWhenNoAdminRoleIsAssigned() {
|
||||
final var response = getUiRealmInfo(keycloakAdminClientWithoutAdminRoles.tokenManager());
|
||||
|
||||
assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||
}
|
||||
|
||||
private static String getKeycloakServerUrl() {
|
||||
return getAuthServerContextRoot() + "/auth";
|
||||
}
|
||||
|
||||
private static UserRepresentation createTestUserRep(final String username, final String clientId, final String roleName) {
|
||||
return UserBuilder.create().enabled(true)
|
||||
.username(username)
|
||||
.email(username + "@localhost")
|
||||
.firstName(username + "-first")
|
||||
.lastName(username + "-last")
|
||||
.password(TEST_PWD)
|
||||
.role(clientId, roleName)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Response getUiRealmInfo(final TokenManager tokenManager) {
|
||||
return prepareHttpRequest(TEST_REALM_NAME, "ui-ext/info", tokenManager)
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.get();
|
||||
}
|
||||
|
||||
private void updateRealmExt(UIRealmRepresentation rep) {
|
||||
try (Client client = Keycloak.getClientProvider().newRestEasyClient(null, null, true)) {
|
||||
Response response = client.target(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth")
|
||||
.path("/admin/realms/" + rep.getRealm() + "/ui-ext")
|
||||
.register(new BearerAuthFilter(adminClient.tokenManager()))
|
||||
final var realmName = rep.getRealm();
|
||||
final var request = prepareHttpRequest(realmName, "ui-ext", adminClient.tokenManager());
|
||||
|
||||
final var response = request
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.put(Entity.entity(rep, MediaType.APPLICATION_JSON));
|
||||
Assert.assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
|
||||
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
|
||||
}
|
||||
|
||||
private WebTarget prepareHttpRequest(final String realmName, final String subPath, final TokenManager tokenManager) {
|
||||
final var realmAdminPath = "/admin/realms/" + realmName;
|
||||
return httpClient.target(getKeycloakServerUrl())
|
||||
.path(realmAdminPath + "/" + subPath)
|
||||
.register(new BearerAuthFilter(tokenManager));
|
||||
}
|
||||
|
||||
private UIRealmRepresentation toUIRealmRepresentation(RealmRepresentation realm, UPConfig upConfig) throws IOException {
|
||||
UIRealmRepresentation uiRealm = JsonSerialization.readValue(JsonSerialization.writeValueAsString(realm), UIRealmRepresentation.class);
|
||||
UIRealmRepresentation uiRealm = deserialize(JsonSerialization.writeValueAsString(realm), UIRealmRepresentation.class);
|
||||
uiRealm.setUpConfig(upConfig);
|
||||
return uiRealm;
|
||||
}
|
||||
|
||||
private UPConfig toUpConfig(String representation) throws IOException {
|
||||
return JsonSerialization.readValue(representation, UPConfig.class);
|
||||
private UPConfig toUpConfig(final String representation) {
|
||||
return deserialize(representation, UPConfig.class);
|
||||
}
|
||||
|
||||
private UIRealmInfo toUiRealmInfo(final String representation) {
|
||||
return deserialize(representation, UIRealmInfo.class);
|
||||
}
|
||||
|
||||
private static <T> T deserialize(final String representation, final Class<T> type) {
|
||||
try {
|
||||
return JsonSerialization.readValue(representation, type);
|
||||
} catch (final IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue