Now uses the role mapping component (#3184)

This commit is contained in:
Erik Jan de Wit 2022-09-16 10:58:43 +02:00 committed by GitHub
parent 7a556a2e1e
commit 05a660b681
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 114 additions and 391 deletions

View file

@ -458,7 +458,6 @@ describe("Clients test", () => {
});
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
commonPage.sidebar().goToClients();
commonPage.tableToolbarUtils().searchItem(client);
@ -591,24 +590,18 @@ describe("Clients test", () => {
commonPage.tableToolbarUtils().searchItem(itemId, false);
commonPage.tableUtils().clickRowItemLink(itemId);
rolesTab.goToAssociatedRolesTab();
commonPage.tableUtils().selectRowItemAction("create-realm", "Remove");
commonPage.tableUtils().selectRowItemAction("create-realm", "Unassign");
commonPage.sidebar().waitForPageLoad();
commonPage
.modalUtils()
.checkModalTitle("Remove associated role?")
.confirmModal();
commonPage.modalUtils().checkModalTitle("Remove mapping?").confirmModal();
commonPage.sidebar().waitForPageLoad();
commonPage
.masthead()
.checkNotificationMessage("Associated roles have been removed", true);
.checkNotificationMessage("Scope mapping successfully removed", true);
commonPage.tableUtils().selectRowItemAction("manage-consent", "Remove");
commonPage.tableUtils().selectRowItemAction("manage-consent", "Unassign");
commonPage.sidebar().waitForPageLoad();
commonPage
.modalUtils()
.checkModalTitle("Remove associated role?")
.confirmModal();
commonPage.modalUtils().checkModalTitle("Remove mapping?").confirmModal();
});
it("Should delete associated role from search bar test", () => {
@ -617,7 +610,7 @@ describe("Clients test", () => {
commonPage.sidebar().waitForPageLoad();
rolesTab.goToAssociatedRolesTab();
cy.get('td[data-label="Role name"]')
cy.get('td[data-label="Name"]')
.contains("manage-account")
.parent()
.within(() => {
@ -627,15 +620,12 @@ describe("Clients test", () => {
associatedRolesPage.removeAssociatedRoles();
commonPage.sidebar().waitForPageLoad();
commonPage
.modalUtils()
.checkModalTitle("Remove associated roles?")
.confirmModal();
commonPage.modalUtils().checkModalTitle("Remove mapping?").confirmModal();
commonPage.sidebar().waitForPageLoad();
commonPage
.masthead()
.checkNotificationMessage("Associated roles have been removed", true);
.checkNotificationMessage("Scope mapping successfully removed", true);
});
it("Should delete client role test", () => {

View file

@ -156,11 +156,11 @@ describe("Realm roles test", () => {
rolesTab.goToAssociatedRolesTab();
listingPage.removeItem("create-realm");
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove associated role?").confirmModal();
modalUtils.checkModalTitle("Remove mapping?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Associated roles have been removed",
"Scope mapping successfully removed",
true
);
});
@ -175,11 +175,11 @@ describe("Realm roles test", () => {
associatedRolesPage.removeAssociatedRoles();
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove associated roles?").confirmModal();
modalUtils.checkModalTitle("Remove mapping?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Associated roles have been removed",
"Scope mapping successfully removed",
true
);
});
@ -206,20 +206,20 @@ describe("Realm roles test", () => {
// delete associated roles from list
listingPage.removeItem("create-realm");
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove associated role?").confirmModal();
modalUtils.checkModalTitle("Remove mapping?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Associated roles have been removed",
"Scope mapping successfully removed",
true
);
listingPage.removeItem("offline_access");
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove associated role?").confirmModal();
modalUtils.checkModalTitle("Remove mapping?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Associated roles have been removed",
"Scope mapping successfully removed",
true
);
});

View file

@ -45,16 +45,14 @@ describe("Realm settings - User registration tab", () => {
it("Remove admin role", () => {
const role = "admin";
listingPage.markItemRow(role).removeMarkedItems();
listingPage.markItemRow(role).removeMarkedItems("Unassign");
sidebarPage.waitForPageLoad();
modalUtils
.checkModalTitle("Remove associated roles?")
.checkModalMessage(
"This action will remove the associated roles of default-roles-master. Users who have permission to default-roles-master will no longer have access to these roles."
)
.checkModalTitle("Remove mapping?")
.checkModalMessage("Are you sure you want to remove this mapping?")
.checkConfirmButtonText("Remove")
.confirmModal();
masthead.checkNotificationMessage("Associated roles have been removed");
masthead.checkNotificationMessage("Scope mapping successfully removed");
});
it("Add default group", () => {

View file

@ -162,8 +162,8 @@ export default class ListingPage extends CommonElements {
return this;
}
removeMarkedItems() {
cy.get(this.listHeaderSecondaryBtn).contains("Remove").click();
removeMarkedItems(name: string = "Remove") {
cy.get(this.listHeaderSecondaryBtn).contains(name).click();
return this;
}
@ -246,7 +246,7 @@ export default class ListingPage extends CommonElements {
removeItem(itemName: string) {
this.clickRowDetails(itemName);
this.clickDetailMenu("Remove");
this.clickDetailMenu("Unassign");
return this;
}

View file

@ -9,7 +9,7 @@ enum ClientRolesTabItems {
export default class ClientRolesTab extends CommonPage {
private createRoleBtn = "create-role";
private createRoleEmptyStateBtn = "no-roles-for-this-client-empty-action";
private hideInheritedRolesChkBox = "#kc-hide-inherited-roles-checkbox";
private hideInheritedRolesChkBox = "#hideInheritedRoles";
private rolesTab = "rolesTab";
private associatedRolesTab = ".kc-associated-roles-tab > button";

View file

@ -28,9 +28,11 @@ export default class DedicatedScopesMappersTab extends CommonPage {
}
addPredefinedMapper() {
this.emptyState()
.checkIfExists(true)
.clickSecondaryBtn(mapperTypeEmptyState.AddPredefinedMapper, true);
this.emptyState().checkIfExists(true);
this.emptyState().clickSecondaryBtn(
mapperTypeEmptyState.AddPredefinedMapper,
true
);
return this;
}

View file

@ -1,14 +1,14 @@
export default class AssociatedRolesPage {
private actionDropdown = "action-dropdown";
private addRolesDropdownItem = "add-roles";
private addRoleToolbarButton = "add-role-button";
private addRoleToolbarButton = "assignRole";
private addAssociatedRolesModalButton = "assign";
private compositeRoleBadge = "composite-role-badge";
private filterTypeDropdown = "filter-type-dropdown";
private filterTypeDropdownItem = "roles";
private usersPage = "users-page";
private removeRolesButton = "removeRoles";
private addRoleTable = 'td[data-label="Name"]';
private removeRolesButton = "unAssignRole";
private addRoleTable = '[aria-label="Roles"] td[data-label="Name"]';
addAssociatedRealmRole(roleName: string) {
cy.findByTestId(this.actionDropdown).last().click();

View file

@ -1,7 +1,7 @@
export default class UserRegistration {
private userRegistrationTab = "rs-userRegistration-tab";
private defaultGroupTab = "#pf-tab-20-groups";
private addRoleBtn = "add-role-button";
private addRoleBtn = "assignRole";
private addDefaultGroupBtn = "no-default-groups-empty-action";
private namesColumn = 'tbody td[data-label="Name"]:visible';
private addBtn = "assign";

View file

@ -30,7 +30,7 @@
"roleCreateError": "Could not create role: {{error}}",
"roleImportSuccess": "Role import successful",
"roleDeleteConfirm": "Delete role?",
"roleDeleteConfirmDialog": "This action will permanently delete the role {{selectedRoleName}} and cannot be undone.",
"roleDeleteConfirmDialog": "This action will permanently delete the role \"{{selectedRoleName}}\" and cannot be undone.",
"roleDeletedSuccess": "The role has been deleted",
"roleDeleteError": "Could not delete role: {{error}}",
"defaultRole": "This role serves as a container for both realm and client default roles. It cannot be removed.",

View file

@ -13,10 +13,8 @@ export type ResourcesKey = keyof KeycloakAdminClient;
type DeleteFunctions =
| keyof Pick<Groups, "delClientRoleMappings" | "delRealmRoleMappings">
| keyof Pick<
ClientScopes,
"delClientScopeMappings" | "delRealmScopeMappings"
>;
| keyof Pick<ClientScopes, "delClientScopeMappings" | "delRealmScopeMappings">
| keyof Pick<Roles, "delCompositeRoles">;
type ListEffectiveFunction =
| keyof Pick<Groups, "listRoleMappings" | "listAvailableRealmRoleMappings">
@ -26,7 +24,7 @@ type ListEffectiveFunction =
| "listAvailableRealmScopeMappings"
| "listCompositeClientScopeMappings"
>
| keyof Pick<Roles, "getCompositeRoles">
| keyof Pick<Roles, "getCompositeRoles" | "getCompositeRolesForClient">
| keyof Pick<
Users,
"listCompositeClientRoleMappings" | "listCompositeRealmRoleMappings"
@ -83,8 +81,12 @@ const mapping: ResourceMapping = {
clientScopes: clientFunctions,
clients: clientFunctions,
roles: {
delete: [],
listEffective: ["getCompositeRoles", "getCompositeRoles"],
delete: ["delCompositeRoles", "delCompositeRoles"],
listEffective: [
"getCompositeRoles",
"getCompositeRoles",
"getCompositeRolesForClient",
],
listAvailable: ["listRoles", "find"],
},
};
@ -140,7 +142,28 @@ export const getMapping = async (
id: string
): Promise<MappingsRepresentation> => {
const query = mapping[type]!.listEffective[0];
return applyQuery(adminClient, type, query, { id }) as MappingsRepresentation;
const result = applyQuery(adminClient, type, query, { id });
if (type !== "roles") {
return result as MappingsRepresentation;
}
const roles = await result;
const clientRoles = await Promise.all(
roles
.filter((r) => r.clientRole)
.map(async (role) => {
const client = await adminClient.clients.findOne({
id: role.containerId!,
});
role.containerId = client?.clientId;
return { ...client, mappings: [role] };
})
);
return {
clientMappings: clientRoles,
realmMappings: roles.filter((r) => !r.clientRole),
};
};
export const getEffectiveRoles = async (
@ -149,9 +172,18 @@ export const getEffectiveRoles = async (
id: string
): Promise<Row[]> => {
const query = mapping[type]!.listEffective[1];
return (await applyQuery(adminClient, type, query, { id })).map((role) => ({
role,
}));
if (type !== "roles") {
return (await applyQuery(adminClient, type, query, { id })).map((role) => ({
role,
}));
}
const roles = await applyQuery(adminClient, type, query, { id });
const parentRoles = await Promise.all(
roles
.filter((r) => r.composite)
.map((r) => applyQuery(adminClient, type, query, { id: r.id }))
);
return [...roles, ...parentRoles.flat()].map((role) => ({ role }));
};
export const getAvailableRoles = async (

View file

@ -1,326 +0,0 @@
import { useState } from "react";
import { useParams, useRouteMatch } from "react-router-dom";
import { useNavigate } from "react-router-dom-v5-compat";
import { useTranslation } from "react-i18next";
import {
AlertVariant,
Button,
ButtonVariant,
Checkbox,
Label,
PageSection,
ToolbarItem,
} from "@patternfly/react-core";
import {
ClientRoleParams,
ClientRoleRoute,
toClientRole,
} from "./routes/ClientRole";
import {
RealmSettingsParams,
RealmSettingsRoute,
} from "../realm-settings/routes/RealmSettings";
import { RealmRoleParams, RealmRoleTab, toRealmRole } from "./routes/RealmRole";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter } from "../util";
import { AddRoleMappingModal } from "../components/role-mapping/AddRoleMappingModal";
import { useAdminClient } from "../context/auth/AdminClient";
import type { AttributeForm } from "../components/key-value-form/AttributeForm";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
type AssociatedRolesTabProps = {
parentRole: AttributeForm;
client?: ClientRepresentation;
refresh: () => void;
};
type Role = RoleRepresentation & {
inherited?: string;
};
export const AssociatedRolesTab = ({
parentRole,
refresh: refreshParent,
}: AssociatedRolesTabProps) => {
const { t } = useTranslation("roles");
const navigate = useNavigate();
const { addAlert, addError } = useAlerts();
const { id, realm } = useParams<RealmRoleParams>();
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
const [isInheritedHidden, setIsInheritedHidden] = useState(false);
const [count, setCount] = useState(0);
const [open, setOpen] = useState(false);
const { adminClient } = useAdminClient();
const clientRoleRouteMatch = useRouteMatch<ClientRoleParams>(
ClientRoleRoute.path
);
const realmSettingsMatch = useRouteMatch<RealmSettingsParams>(
RealmSettingsRoute.path
);
const subRoles = async (result: Role[], roles: Role[]): Promise<Role[]> => {
const promises = roles.map(async (r) => {
if (result.find((o) => o.id === r.id)) return result;
result.push(r);
if (r.composite) {
const subList = (await adminClient.roles.getCompositeRoles({
id: r.id!,
})) as Role[];
subList.map((o) => (o.inherited = r.name));
result.concat(await subRoles(result, subList));
}
return result;
});
await Promise.all(promises);
return [...result];
};
const loader = async (first?: number, max?: number, search?: string) => {
const compositeRoles = await adminClient.roles.getCompositeRoles({
id: parentRole.id!,
first: first,
max: max!,
search: search,
});
setCount(compositeRoles.length);
if (!isInheritedHidden) {
const children = await subRoles([], compositeRoles);
compositeRoles.splice(0, compositeRoles.length);
compositeRoles.push(...children);
}
await Promise.all(
compositeRoles.map(async (role) => {
if (role.clientRole) {
role.containerId = (
await adminClient.clients.findOne({
id: role.containerId!,
})
)?.clientId;
}
})
);
return compositeRoles;
};
const toRolesTab = (tab: RealmRoleTab | undefined = "associated-roles") => {
const to = clientRoleRouteMatch
? toClientRole({ ...clientRoleRouteMatch.params, tab })
: !realmSettingsMatch
? toRealmRole({
realm,
id,
tab,
})
: undefined;
if (to) navigate(to);
};
const AliasRenderer = ({ id, name, clientRole, containerId }: Role) => {
return (
<>
{clientRole && (
<Label color="blue" key={`label-${id}`}>
{containerId}
</Label>
)}{" "}
{name}
</>
);
};
const toggleModal = () => {
setOpen(!open);
};
const reload = () => {
if (selectedRows.length >= count) {
refreshParent();
toRolesTab("details");
} else {
refresh();
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleRemoveAssociatedRoleConfirm",
messageKey: t("roles:roleRemoveAssociatedText", {
role: selectedRows.map((r) => r.name),
roleName: parentRole.name,
}),
continueButtonLabel: "common:remove",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.roles.delCompositeRoles(
{ id: parentRole.id! },
selectedRows
);
reload();
setSelectedRows([]);
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
} catch (error) {
addError("roles:roleDeleteError", error);
}
},
});
const [toggleDeleteAssociatedRolesDialog, DeleteAssociatedRolesConfirm] =
useConfirmDialog({
titleKey: t("roles:removeAssociatedRoles") + "?",
messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", {
name: parentRole.name || t("createRole"),
}),
continueButtonLabel: "common:remove",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.roles.delCompositeRoles(
{ id: parentRole.id! },
selectedRows
);
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
reload();
} catch (error) {
addError("roles:roleDeleteError", error);
}
},
});
const addComposites = async (composites: RoleRepresentation[]) => {
const compositeArray = composites;
try {
await adminClient.roles.createComposite(
{ roleId: parentRole.id!, realm },
compositeArray
);
toRolesTab();
refresh();
addAlert(t("addAssociatedRolesSuccess"), AlertVariant.success);
} catch (error) {
addError("roles:addAssociatedRolesError", error);
}
};
return (
<PageSection variant="light" padding={{ default: "noPadding" }}>
<DeleteConfirm />
<DeleteAssociatedRolesConfirm />
{open && (
<AddRoleMappingModal
id={parentRole.id!}
type="roles"
name={parentRole.name}
onAssign={(rows) => addComposites(rows.map((r) => r.role))}
onClose={() => setOpen(false)}
/>
)}
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor"
canSelectAll
isPaginated
onSelect={(rows) => {
setSelectedRows([
...rows.map((r) => {
delete r.inherited;
return r;
}),
]);
}}
toolbarItem={
<>
<ToolbarItem>
<Checkbox
label="Hide inherited roles"
key="associated-roles-check"
id="kc-hide-inherited-roles-checkbox"
onChange={() => {
setIsInheritedHidden(!isInheritedHidden);
refresh();
}}
isChecked={isInheritedHidden}
/>
</ToolbarItem>
<ToolbarItem>
<Button
key="add-role-button"
onClick={() => toggleModal()}
data-testid="add-role-button"
>
{t("addRole")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Button
variant="link"
isDisabled={selectedRows.length === 0}
key="remove-role-button"
data-testid="removeRoles"
onClick={() => {
toggleDeleteAssociatedRolesDialog();
}}
>
{t("removeRoles")}
</Button>
</ToolbarItem>
</>
}
actions={[
{
title: t("common:remove"),
onRowClick: (role) => {
setSelectedRows([role]);
toggleDeleteDialog();
},
},
]}
columns={[
{
name: "name",
displayKey: "roles:roleName",
cellRenderer: AliasRenderer,
cellFormatters: [emptyFormatter()],
},
{
name: "inherited",
displayKey: "roles:inheritedFrom",
cellFormatters: [emptyFormatter()],
},
{
name: "description",
displayKey: "common:description",
cellFormatters: [emptyFormatter()],
},
]}
emptyState={
<ListEmptyState
hasIcon
message={t("noRolesAssociated")}
instructions={t("noRolesAssociatedInstructions")}
primaryActionText={t("addRole")}
onPrimaryAction={() => setOpen(true)}
/>
}
/>
</PageSection>
);
};

View file

@ -31,7 +31,6 @@ import { RealmRoleForm } from "./RealmRoleForm";
import { useRealm } from "../context/realm-context/RealmContext";
import { AddRoleMappingModal } from "../components/role-mapping/AddRoleMappingModal";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { AssociatedRolesTab } from "./AssociatedRolesTab";
import { UsersInRoleTab } from "./UsersInRoleTab";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { toRealmRole } from "./routes/RealmRole";
@ -41,6 +40,7 @@ import {
toClientRole,
} from "./routes/ClientRole";
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import { RoleMapping } from "../components/role-mapping/RoleMapping";
export default function RealmRoleTabs() {
const { t } = useTranslation("roles");
@ -59,10 +59,10 @@ export default function RealmRoleTabs() {
const { realm: realmName } = useRealm();
const [key, setKey] = useState("");
const [key, setKey] = useState(0);
const refresh = () => {
setKey(`${new Date().getTime()}`);
setKey(key + 1);
};
const { addAlert, addError } = useAlerts();
@ -184,7 +184,7 @@ export default function RealmRoleTabs() {
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", {
name: role?.name || t("createRole"),
selectedRoleName: role?.name || t("createRole"),
}),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
@ -385,7 +385,13 @@ export default function RealmRoleTabs() {
className="kc-associated-roles-tab"
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
>
<AssociatedRolesTab parentRole={role} refresh={refresh} />
<RoleMapping
name={role.name!}
id={role.id!}
type="roles"
isManager
save={(rows) => addComposites(rows.map((r) => r.role))}
/>
</Tab>
)}
{!isDefaultRole(role.name!) && (

View file

@ -1,12 +1,14 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import { AlertVariant, Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { AssociatedRolesTab } from "../realm-roles/AssociatedRolesTab";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { useAlerts } from "../components/alert/Alerts";
import { RoleMapping } from "../components/role-mapping/RoleMapping";
import { DefaultsGroupsTab } from "./DefaultGroupsTab";
export const UserRegistration = () => {
@ -16,6 +18,7 @@ export const UserRegistration = () => {
const [key, setKey] = useState(0);
const { adminClient } = useAdminClient();
const { addAlert, addError } = useAlerts();
const { realm: realmName } = useRealm();
useFetch(
@ -28,6 +31,21 @@ export const UserRegistration = () => {
return <KeycloakSpinner />;
}
const addComposites = async (composites: RoleRepresentation[]) => {
const compositeArray = composites;
try {
await adminClient.roles.createComposite(
{ roleId: realm.defaultRole!.id!, realm: realmName },
compositeArray
);
setKey(key + 1);
addAlert(t("roles:addAssociatedRolesSuccess"), AlertVariant.success);
} catch (error) {
addError("roles:addAssociatedRolesError", error);
}
};
return (
<Tabs
activeKey={activeTab}
@ -39,9 +57,12 @@ export const UserRegistration = () => {
eventKey={10}
title={<TabTitleText>{t("defaultRoles")}</TabTitleText>}
>
<AssociatedRolesTab
parentRole={{ ...realm.defaultRole, attributes: [] }}
refresh={() => setKey(key + 1)}
<RoleMapping
name={realm.defaultRole!.name!}
id={realm.defaultRole!.id!}
type="roles"
isManager
save={(rows) => addComposites(rows.map((r) => r.role))}
/>
</Tab>
<Tab