refactor + use new composite role api (#1399)

* refactor + use new composite role api

* fixed types

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/RealmRoleTabs.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* code review comments

* removed duplicate type

* route

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/routes/RealmRole.ts

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* fix unused import

* fixed test

* fixed merge errors

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2021-11-01 13:29:52 +01:00 committed by GitHub
parent e22b27b471
commit 9c48bb4d90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 347 additions and 409 deletions

View file

@ -12,12 +12,8 @@ export default class AssociatedRolesPage {
addAssociatedRealmRole() {
cy.findByTestId(this.actionDropdown).last().click();
const load = "/auth/admin/realms/master/clients";
cy.intercept(load).as("load");
cy.findByTestId(this.addRolesDropdownItem).click();
cy.wait(["@load"]);
cy.get(this.checkbox).eq(2).check();
cy.findByTestId(this.addAssociatedRolesModalButton).contains("Add").click();
@ -29,8 +25,6 @@ export default class AssociatedRolesPage {
"Composite"
);
cy.wait(["@load"]);
return this;
}

View file

@ -60,13 +60,12 @@ const SecuredRoute = ({ route }: SecuredRouteProps) => {
? hasAccess(...route.access)
: hasAccess(route.access);
if (accessAllowed) {
if (accessAllowed)
return (
<Suspense fallback={<Spinner />}>
<route.component />
</Suspense>
);
}
return <ForbiddenSection />;
};

View file

@ -12,14 +12,15 @@ import {
} from "@patternfly/react-table";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import type { RoleRepresentation } from "../../model/role-model";
import { FormAccess } from "../form-access/FormAccess";
import "./attribute-form.css";
export type KeyValueType = { key: string; value: string };
export type AttributeForm = {
attributes: KeyValueType[];
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
attributes?: KeyValueType[];
};
export type AttributesFormProps = {

View file

@ -47,7 +47,7 @@ export const GroupAttributes = () => {
const save = async (attributeForm: AttributeForm) => {
try {
const group = currentGroup();
const attributes = arrayToAttributes(attributeForm.attributes);
const attributes = arrayToAttributes(attributeForm.attributes!);
await adminClient.groups.update({ id: id! }, { ...group, attributes });
setSubGroups([

View file

@ -21,8 +21,8 @@ import { useRealm } from "../../context/realm-context/RealmContext";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import {
AttributeForm,
AttributesForm,
KeyValueType,
} from "../../components/attribute-form/AttributeForm";
import { FormAccess } from "../../components/form-access/FormAccess";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
@ -39,9 +39,7 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { groupBy } from "lodash";
export type IdPMapperRepresentationWithAttributes =
IdentityProviderMapperRepresentation & {
attributes: KeyValueType[];
};
IdentityProviderMapperRepresentation & AttributeForm;
type Role = RoleRepresentation & {
clientId?: string;
@ -86,7 +84,6 @@ export default function AddMapper() {
const [currentMapper, setCurrentMapper] =
useState<IdentityProviderMapperRepresentation>();
const [roles, setRoles] = useState<RoleRepresentation[]>([]);
const [rolesModalOpen, setRolesModalOpen] = useState(false);
@ -144,19 +141,14 @@ export default function AddMapper() {
Promise.all([
id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
adminClient.identityProviders.findMapperTypes({ alias }),
!id ? adminClient.roles.find() : null,
]),
([mapper, mapperTypes, roles]) => {
([mapper, mapperTypes]) => {
if (mapper) {
setCurrentMapper(mapper);
setupForm(mapper);
}
setMapperTypes(mapperTypes);
if (roles) {
setRoles(roles);
}
},
[]
);
@ -257,17 +249,15 @@ export default function AddMapper() {
}
divider
/>
<AssociatedRolesModal
onConfirm={(role: Role[]) => {
setSelectedRole(role);
}}
allRoles={roles}
open={rolesModalOpen}
omitComposites
isRadio
isMapperId
toggleDialog={toggleModal}
/>
{rolesModalOpen && (
<AssociatedRolesModal
onConfirm={(role) => setSelectedRole(role)}
omitComposites
isRadio
isMapperId
toggleDialog={toggleModal}
/>
)}
<FormAccess
role="manage-identity-providers"
isHorizontal

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import {
AlertVariant,
Button,
Dropdown,
DropdownItem,
@ -8,6 +9,7 @@ import {
Label,
Modal,
ModalVariant,
Spinner,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useFetch, useAdminClient } from "../context/auth/AdminClient";
@ -15,76 +17,74 @@ import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/ro
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
import _ from "lodash";
import type { RealmRoleParams } from "./routes/RealmRole";
import { omit, sortBy } from "lodash";
import { RealmRoleParams, toRealmRole } from "./routes/RealmRole";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAlerts } from "../components/alert/Alerts";
import {
ClientRoleParams,
ClientRoleRoute,
toClientRole,
} from "./routes/ClientRole";
type Role = RoleRepresentation & {
clientId?: string;
};
export type AssociatedRolesModalProps = {
open: boolean;
toggleDialog: () => void;
onConfirm: (newReps: RoleRepresentation[]) => void;
existingCompositeRoles?: RoleRepresentation[];
allRoles?: RoleRepresentation[];
onConfirm?: (newReps: RoleRepresentation[]) => void;
omitComposites?: boolean;
isRadio?: boolean;
isMapperId?: boolean;
};
export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
type FilterType = "roles" | "clients";
export const AssociatedRolesModal = ({
toggleDialog,
onConfirm,
omitComposites,
isRadio,
isMapperId,
}: AssociatedRolesModalProps) => {
const { t } = useTranslation("roles");
const [name, setName] = useState("");
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const { realm } = useRealm();
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
const [compositeRoles, setCompositeRoles] = useState<RoleRepresentation[]>();
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
const [filterType, setFilterType] = useState("roles");
const [filterType, setFilterType] = useState<FilterType>("roles");
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const { id } = useParams<RealmRoleParams>();
const clientRoleRouteMatch = useRouteMatch<ClientRoleParams>(
ClientRoleRoute.path
);
const history = useHistory();
const alphabetize = (rolesList: RoleRepresentation[]) => {
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
return sortBy(rolesList, (role) => role.name?.toUpperCase());
};
const loader = async () => {
const roles = await adminClient.roles.find();
const loader = async (first?: number, max?: number, search?: string) => {
const params: { [name: string]: string | number } = {
first: first!,
max: max!,
};
if (!props.omitComposites) {
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
{
id,
}
);
const allRoles = [...roles, ...existingAdditionalRoles];
const searchParam = search || "";
const filterDupes: Role[] = allRoles.filter(
(thing, index, self) =>
index === self.findIndex((t) => t.name === thing.name)
);
const clients = await adminClient.clients.find();
filterDupes
.filter((role) => role.clientRole)
.map(
(role) =>
(role.clientId = clients.find(
(client) => client.id === role.containerId
)!.clientId!)
);
return alphabetize(filterDupes).filter((role: RoleRepresentation) => {
return (
props.existingCompositeRoles?.find(
(existing: RoleRepresentation) => existing.name === role.name
) === undefined && role.name !== name
);
});
if (searchParam) {
params.search = searchParam;
}
return alphabetize(roles);
return adminClient.roles.find(params);
};
const AliasRenderer = ({ id, name, clientId }: Role) => {
@ -100,45 +100,44 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
);
};
/* this is still pretty expensive querying all client and then all roles */
const clientRolesLoader = async () => {
const clients = await adminClient.clients.find();
const clientRoles = await Promise.all(
clients.map(async (client) => {
const roles = await adminClient.clients.listRoles({ id: client.id! });
const clientIdArray = clients.map((client) => client.id);
return roles.map<Role>((role) => ({
...role,
clientId: client.clientId,
}));
})
);
let rolesList: Role[] = [];
for (const id of clientIdArray) {
const clientRolesList = await adminClient.clients.listRoles({
id: id as string,
});
rolesList = [...rolesList, ...clientRolesList];
}
return alphabetize(clientRoles.flat());
};
rolesList
.filter((role) => role.clientRole)
.map(
(role) =>
(role.clientId = clients.find(
(client) => client.id === role.containerId
)!.clientId!)
);
const addComposites = async (composites: RoleRepresentation[]) => {
const compositeArray = composites;
if (!props.omitComposites) {
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
{
const to = clientRoleRouteMatch
? toClientRole({ ...clientRoleRouteMatch.params, tab: "AssociateRoles" })
: toRealmRole({
realm,
id,
}
tab: "AssociatedRoles",
});
try {
await adminClient.roles.createComposite(
{ roleId: id, realm },
compositeArray
);
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
return (
existingAdditionalRoles.find(
(existing: RoleRepresentation) => existing.name === role.name
) === undefined && role.name !== name
);
});
history.push(to);
addAlert(t("addAssociatedRolesSuccess"), AlertVariant.success);
} catch (error) {
addError("roles:addAssociatedRolesError", error);
}
return alphabetize(rolesList);
};
useEffect(() => {
@ -146,11 +145,18 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
}, [filterType]);
useFetch(
() =>
!props.isMapperId
? adminClient.roles.findOneById({ id })
: Promise.resolve(null),
(role) => setName(role ? role.name! : t("createRole")),
async () => {
const [role, compositeRoles] = await Promise.all([
!isMapperId ? adminClient.roles.findOneById({ id }) : undefined,
!omitComposites ? adminClient.roles.getCompositeRoles({ id }) : [],
]);
return { role, compositeRoles };
},
({ role, compositeRoles }) => {
setName(role ? role.name! : t("createRole"));
setCompositeRoles(compositeRoles);
},
[]
);
@ -168,12 +174,20 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
setIsFilterDropdownOpen(!isFilterDropdownOpen);
};
if (!compositeRoles) {
return (
<div className="pf-u-text-align-center">
<Spinner />
</div>
);
}
return (
<Modal
data-testid="addAssociatedRole"
title={t("roles:associatedRolesModalTitle", { name })}
isOpen={props.open}
onClose={props.toggleDialog}
isOpen
onClose={toggleDialog}
variant={ModalVariant.large}
actions={[
<Button
@ -182,8 +196,12 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
variant="primary"
isDisabled={!selectedRows.length}
onClick={() => {
props.toggleDialog();
props.onConfirm(selectedRows);
toggleDialog();
if (onConfirm) {
onConfirm(selectedRows);
} else {
addComposites(selectedRows);
}
}}
>
{t("common:add")}
@ -192,7 +210,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
key="cancel"
variant="link"
onClick={() => {
props.toggleDialog();
toggleDialog();
}}
>
{t("common:cancel")}
@ -204,7 +222,9 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
loader={filterType === "roles" ? loader : clientRolesLoader}
ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor"
isRadio={props.isRadio}
isRadio={isRadio}
isPaginated={filterType === "roles"}
isRowDisabled={(r) => compositeRoles.some((o) => o.name === r.name)}
searchTypeComponent={
<Dropdown
onSelect={() => onFilterDropdownSelect(filterType)}
@ -234,7 +254,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
}
canSelectAll
onSelect={(rows) => {
setSelectedRows([...rows]);
setSelectedRows(rows.map((r) => omit(r, "clientId")));
}}
columns={[
{

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
@ -18,27 +18,22 @@ import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter } from "../util";
import { AssociatedRolesModal } from "./AssociatedRolesModal";
import { useAdminClient } from "../context/auth/AdminClient";
import type { RoleFormType } from "./RealmRoleTabs";
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import _ from "lodash";
type AssociatedRolesTabProps = {
additionalRoles: Role[];
addComposites: (newReps: RoleRepresentation[]) => void;
parentRole: RoleFormType;
onRemove: (newReps: RoleRepresentation[]) => void;
parentRole: AttributeForm;
client?: ClientRepresentation;
refresh: () => void;
};
type Role = RoleRepresentation & {
clientId?: string;
inherited?: string;
};
export const AssociatedRolesTab = ({
additionalRoles,
addComposites,
parentRole,
onRemove,
refresh: refreshParent,
}: AssociatedRolesTabProps) => {
const { t } = useTranslation("roles");
const history = useHistory();
@ -49,100 +44,65 @@ export const AssociatedRolesTab = ({
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
const [isInheritedHidden, setIsInheritedHidden] = useState(false);
const [allRoles, setAllRoles] = useState<RoleRepresentation[]>([]);
const [count, setCount] = useState(0);
const [open, setOpen] = useState(false);
const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>();
const inheritanceMap = React.useRef<{ [key: string]: string }>({});
const getSubRoles = async (role: Role, allRoles: Role[]): Promise<Role[]> => {
// Fetch all composite roles
const allCompositeRoles = await adminClient.roles.getCompositeRoles({
id: role.id!,
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;
});
// Need to ensure we don't get into an infinite loop, do not add any role that is already there or the starting role
const newRoles: Promise<RoleRepresentation[]> = allCompositeRoles.reduce(
async (acc: Promise<RoleRepresentation[]>, newRole) => {
const resolvedRoles = await acc;
if (!allRoles.find((ar) => ar.id === newRole.id)) {
inheritanceMap.current[newRole.id!] = role.name!;
resolvedRoles.push(newRole);
const subRoles = await getSubRoles(newRole, [
...allRoles,
...resolvedRoles,
]);
resolvedRoles.push(...subRoles);
}
return acc;
},
Promise.resolve([] as RoleRepresentation[])
);
return newRoles;
await Promise.all(promises);
return [...result];
};
const loader = async () => {
const alphabetize = (rolesList: Role[]) => {
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
};
const clients = await adminClient.clients.find();
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) {
setAllRoles(additionalRoles);
return alphabetize(
additionalRoles.filter(
(role) =>
role.containerId === "master" && !inheritanceMap.current[role.id!]
)
);
if (!isInheritedHidden) {
const children = await subRoles([], compositeRoles);
compositeRoles.splice(0, compositeRoles.length);
compositeRoles.push(...children);
}
const fetchedRoles: Promise<Role[]> = additionalRoles.reduce(
async (acc: Promise<Role[]>, role) => {
const resolvedRoles = await acc;
resolvedRoles.push(role);
const subRoles = await getSubRoles(role, resolvedRoles);
resolvedRoles.push(...subRoles);
return acc;
},
Promise.resolve([] as Role[])
await Promise.all(
compositeRoles.map(async (role) => {
if (role.clientRole) {
role.containerId = (
await adminClient.clients.findOne({
id: role.containerId!,
})
)?.clientId;
}
})
);
return fetchedRoles.then((results: Role[]) => {
const filterDupes = results.filter(
(thing, index, self) =>
index === self.findIndex((t) => t.name === thing.name)
);
filterDupes
.filter((role) => role.clientRole)
.map(
(role) =>
(role.clientId = clients.find(
(client) => client.id === role.containerId
)!.clientId!)
);
return alphabetize(additionalRoles);
});
return compositeRoles;
};
useEffect(() => {
refresh();
}, [additionalRoles, isInheritedHidden]);
const InheritedRoleName = (role: RoleRepresentation) =>
inheritanceMap.current[role.id!];
const AliasRenderer = ({ id, name, clientId }: Role) => {
const AliasRenderer = ({ id, name, clientRole, containerId }: Role) => {
return (
<>
{clientId && (
{clientRole && (
<Label color="blue" key={`label-${id}`}>
{clientId}
{containerId}
</Label>
)}{" "}
{name}
@ -150,7 +110,20 @@ export const AssociatedRolesTab = ({
);
};
const toggleModal = () => setOpen(!open);
const toggleModal = () => {
setOpen(!open);
refresh();
};
const reload = () => {
if (selectedRows.length >= count) {
refreshParent();
const loc = url.replace(/\/AssociatedRoles/g, "/details");
history.push(loc);
} else {
refresh();
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleRemoveAssociatedRoleConfirm",
@ -163,7 +136,7 @@ export const AssociatedRolesTab = ({
onConfirm: async () => {
try {
await adminClient.roles.delCompositeRoles({ id }, selectedRows);
onRemove(selectedRows);
reload();
setSelectedRows([]);
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
@ -177,21 +150,15 @@ export const AssociatedRolesTab = ({
useConfirmDialog({
titleKey: t("roles:removeAssociatedRoles") + "?",
messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", {
name: parentRole?.name || t("createRole"),
name: parentRole.name || t("createRole"),
}),
continueButtonLabel: "common:remove",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
if (selectedRows.length >= allRoles.length) {
onRemove(selectedRows);
const loc = url.replace(/\/AssociatedRoles/g, "/details");
history.push(loc);
}
onRemove(selectedRows);
await adminClient.roles.delCompositeRoles({ id }, selectedRows);
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
refresh();
reload();
} catch (error) {
addError("roles:roleDeleteError", error);
}
@ -203,20 +170,21 @@ export const AssociatedRolesTab = ({
<PageSection variant="light" padding={{ default: "noPadding" }}>
<DeleteConfirm />
<DeleteAssociatedRolesConfirm />
<AssociatedRolesModal
onConfirm={addComposites}
existingCompositeRoles={additionalRoles}
open={open}
toggleDialog={() => setOpen(!open)}
/>
{open && <AssociatedRolesModal toggleDialog={toggleModal} />}
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor"
canSelectAll
isPaginated
onSelect={(rows) => {
setSelectedRows([...rows]);
setSelectedRows([
...rows.map((r) => {
delete r.inherited;
return r;
}),
]);
}}
toolbarItem={
<>
@ -225,7 +193,10 @@ export const AssociatedRolesTab = ({
label="Hide inherited roles"
key="associated-roles-check"
id="kc-hide-inherited-roles-checkbox"
onChange={() => setIsInheritedHidden(!isInheritedHidden)}
onChange={() => {
setIsInheritedHidden(!isInheritedHidden);
refresh();
}}
isChecked={isInheritedHidden}
/>
</ToolbarItem>
@ -269,9 +240,8 @@ export const AssociatedRolesTab = ({
cellFormatters: [emptyFormatter()],
},
{
name: "containerId",
name: "inherited",
displayKey: "roles:inheritedFrom",
cellRenderer: InheritedRoleName,
cellFormatters: [emptyFormatter()],
},
{

View file

@ -3,17 +3,18 @@ import {
ActionGroup,
Button,
FormGroup,
PageSection,
TextArea,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import type { UseFormMethods } from "react-hook-form";
import type { RoleFormType } from "./RealmRoleTabs";
import { FormAccess } from "../components/form-access/FormAccess";
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
export type RealmRoleFormProps = {
form: UseFormMethods<RoleFormType>;
form: UseFormMethods<AttributeForm>;
save: () => void;
editMode: boolean;
reset: () => void;
@ -28,66 +29,70 @@ export const RealmRoleForm = ({
const { t } = useTranslation("roles");
return (
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-realm"
className="pf-u-mt-lg"
>
<FormGroup
label={t("roleName")}
fieldId="kc-name"
isRequired
validated={errors.name ? "error" : "default"}
helperTextInvalid={t("common:required")}
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-realm"
className="pf-u-mt-lg"
>
<TextInput
ref={register({ required: !editMode })}
type="text"
id="kc-name"
name="name"
isReadOnly={editMode}
/>
</FormGroup>
<FormGroup
label={t("common:description")}
fieldId="kc-description"
validated={
errors.description ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={errors.description?.message}
>
<TextArea
name="description"
aria-label="description"
isDisabled={getValues().name?.includes("default-roles")}
ref={register({
maxLength: {
value: 255,
message: t("common:maxLength", { length: 255 }),
},
})}
type="text"
<FormGroup
label={t("roleName")}
fieldId="kc-name"
isRequired
validated={errors.name ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: !editMode })}
type="text"
id="kc-name"
name="name"
isReadOnly={editMode}
/>
</FormGroup>
<FormGroup
label={t("common:description")}
fieldId="kc-description"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
id="kc-role-description"
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={save}
data-testid="realm-roles-save-button"
helperTextInvalid={errors.description?.message}
>
{t("common:save")}
</Button>
<Button onClick={() => reset()} variant="link">
{editMode ? t("common:revert") : t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
<TextArea
name="description"
aria-label="description"
isDisabled={getValues().name?.includes("default-roles")}
ref={register({
maxLength: {
value: 255,
message: t("common:maxLength", { length: 255 }),
},
})}
type="text"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
id="kc-role-description"
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={save}
data-testid="realm-roles-save-button"
>
{t("common:save")}
</Button>
<Button onClick={() => reset()} variant="link">
{editMode ? t("common:revert") : t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
);
};

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import {
AlertVariant,
@ -10,16 +10,16 @@ import {
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useFieldArray, useForm } from "react-hook-form";
import { omit } from "lodash";
import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type Composites from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import {
KeyValueType,
AttributesForm,
attributesToArray,
arrayToAttributes,
AttributeForm,
} from "../components/attribute-form/AttributeForm";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
@ -31,10 +31,6 @@ import { AssociatedRolesTab } from "./AssociatedRolesTab";
import { UsersInRoleTab } from "./UsersInRoleTab";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
attributes: KeyValueType[];
};
type myRealmRepresentation = RealmRepresentation & {
defaultRole?: {
id: string;
@ -44,11 +40,14 @@ type myRealmRepresentation = RealmRepresentation & {
export default function RealmRoleTabs() {
const { t } = useTranslation("roles");
const form = useForm<RoleFormType>({ mode: "onChange" });
const form = useForm<AttributeForm>({
mode: "onChange",
});
const { control, setValue, getValues, trigger, reset } = form;
const history = useHistory();
const adminClient = useAdminClient();
const [role, setRole] = useState<RoleFormType>();
const [role, setRole] = useState<AttributeForm>();
const { id, clientId } = useParams<{ id: string; clientId: string }>();
@ -62,10 +61,6 @@ export default function RealmRoleTabs() {
setKey(`${new Date().getTime()}`);
};
const [additionalRoles, setAdditionalRoles] = useState<RoleRepresentation[]>(
[]
);
const { addAlert, addError } = useAlerts();
const [open, setOpen] = useState(false);
@ -80,104 +75,93 @@ export default function RealmRoleTabs() {
const [realm, setRealm] = useState<myRealmRepresentation>();
useFetch(
() => adminClient.realms.findOne({ realm: realmName }),
(realm) => {
if (!realm) {
async () => {
const realm = await adminClient.realms.findOne({ realm: realmName });
if (!id) {
return { realm };
}
const role = await adminClient.roles.findOneById({ id });
return { realm, role };
},
({ realm, role }) => {
if (!realm || (!role && id)) {
throw new Error(t("common:notFound"));
}
if (role) {
const convertedRole = convert(role);
setRole(convertedRole);
Object.entries(convertedRole).map((entry) => {
setValue(entry[0], entry[1]);
});
}
setRealm(realm);
},
[]
[key]
);
useEffect(() => {
const update = async () => {
if (id) {
const fetchedRole = await adminClient.roles.findOneById({ id });
if (!fetchedRole) {
throw new Error(t("common:notFound"));
}
const allAdditionalRoles = await adminClient.roles.getCompositeRoles({
id,
});
setAdditionalRoles(allAdditionalRoles);
const convertedRole = convert(fetchedRole);
Object.entries(convertedRole).map((entry) => {
form.setValue(entry[0], entry[1]);
});
setRole(convertedRole);
}
};
setTimeout(update, 100);
}, [key]);
const { fields, append, remove } = useFieldArray({
control: form.control,
control,
name: "attributes",
});
const save = async () => {
try {
const role = form.getValues();
const values = getValues();
if (
role.attributes &&
role.attributes[role.attributes.length - 1].key === ""
values.attributes &&
values.attributes[values.attributes.length - 1]?.key === ""
) {
form.setValue(
setValue(
"attributes",
role.attributes.slice(0, role.attributes.length - 1)
values.attributes.slice(0, values.attributes.length - 1)
);
}
if (!(await form.trigger())) {
if (!(await trigger())) {
return;
}
const { attributes, ...rest } = role;
const roleRepresentation: RoleRepresentation = rest;
const { attributes, ...rest } = values;
let roleRepresentation: RoleRepresentation = rest;
if (id) {
if (attributes) {
roleRepresentation.attributes = arrayToAttributes(attributes);
}
roleRepresentation = {
...omit(role!, "attributes"),
...roleRepresentation,
};
if (!clientId) {
await adminClient.roles.updateById({ id }, roleRepresentation);
} else {
await adminClient.clients.updateRole(
{ id: clientId, roleName: role.name! },
{ id: clientId, roleName: values.name! },
roleRepresentation
);
}
await adminClient.roles.createComposite(
{ roleId: id, realm: realmName },
additionalRoles
);
setRole(role);
form.reset(role);
setRole(convert(roleRepresentation));
} else {
let createdRole;
if (!clientId) {
await adminClient.roles.create(roleRepresentation);
createdRole = await adminClient.roles.findOneByName({
name: role.name!,
name: values.name!,
});
} else {
await adminClient.clients.createRole({
id: clientId,
name: role.name,
name: values.name,
});
if (role.description) {
if (values.description) {
await adminClient.clients.updateRole(
{ id: clientId, roleName: role.name! },
{ id: clientId, roleName: values.name! },
roleRepresentation
);
}
createdRole = await adminClient.clients.findRole({
id: clientId,
roleName: role.name!,
roleName: values.name!,
});
}
if (!createdRole) {
@ -195,23 +179,6 @@ export default function RealmRoleTabs() {
}
};
const addComposites = async (composites: Composites[]): Promise<void> => {
const compositeArray = composites;
setAdditionalRoles([...additionalRoles, ...compositeArray]);
try {
await adminClient.roles.createComposite(
{ roleId: id, realm: realmName },
compositeArray
);
history.push(url.substr(0, url.lastIndexOf("/") + 1) + "AssociatedRoles");
refresh();
addAlert(t("addAssociatedRolesSuccess"), AlertVariant.success);
} catch (error) {
addError("roles:addAssociatedRolesError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", {
@ -297,6 +264,9 @@ export default function RealmRoleTabs() {
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
const additionalRoles = await adminClient.roles.getCompositeRoles({
id: role!.id!,
});
await adminClient.roles.delCompositeRoles({ id }, additionalRoles);
addAlert(
t("compositeRoleOff"),
@ -312,24 +282,22 @@ export default function RealmRoleTabs() {
},
});
const toggleModal = () => setOpen(!open);
const toggleModal = () => {
setOpen(!open);
refresh();
};
return (
<>
<DeleteConfirm />
<DeleteAllAssociatedRolesConfirm />
<AssociatedRolesModal
onConfirm={addComposites}
existingCompositeRoles={additionalRoles}
open={open}
toggleDialog={() => setOpen(!open)}
/>
{open && <AssociatedRolesModal toggleDialog={toggleModal} />}
<ViewHeader
titleKey={role?.name || t("createRole")}
badges={[
{
id: "composite-role-badge",
text: additionalRoles.length > 0 ? t("composite") : "",
text: role?.composite ? t("composite") : "",
readonly: true,
},
]}
@ -340,38 +308,27 @@ export default function RealmRoleTabs() {
/>
<PageSection variant="light" className="pf-u-p-0">
{id && (
<KeycloakTabs isBox>
<KeycloakTabs isBox mountOnEnter>
<Tab
eventKey="details"
title={<TabTitleText>{t("common:details")}</TabTitleText>}
>
<PageSection variant="light">
<RealmRoleForm
reset={() => form.reset(role)}
form={form}
save={save}
editMode={true}
/>
</PageSection>
<RealmRoleForm
reset={() => reset(role)}
form={form}
save={save}
editMode={true}
/>
</Tab>
{additionalRoles.length > 0 && (
{role?.composite && (
<Tab
eventKey="AssociatedRoles"
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
>
<PageSection variant="light">
{role && (
<AssociatedRolesTab
additionalRoles={additionalRoles}
addComposites={addComposites}
parentRole={role}
onRemove={() => refresh()}
/>
)}
</PageSection>
<AssociatedRolesTab parentRole={role} refresh={refresh} />
</Tab>
)}
{form.getValues().name !== realm?.defaultRole?.name && (
{role?.name !== realm?.defaultRole?.name && (
<Tab
eventKey="attributes"
className="kc-attributes-tab"
@ -381,11 +338,11 @@ export default function RealmRoleTabs() {
form={form}
save={save}
array={{ fields, append, remove }}
reset={() => form.reset(role)}
reset={() => reset(role)}
/>
</Tab>
)}
{form.getValues().name !== realm?.defaultRole?.name && (
{role?.name !== realm?.defaultRole?.name && (
<Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
@ -396,14 +353,12 @@ export default function RealmRoleTabs() {
</KeycloakTabs>
)}
{!id && (
<PageSection variant="light">
<RealmRoleForm
reset={() => form.reset()}
form={form}
save={save}
editMode={false}
/>
</PageSection>
<RealmRoleForm
reset={() => reset(role)}
form={form}
save={save}
editMode={false}
/>
)}
</PageSection>
</>

View file

@ -12,15 +12,15 @@ import {
} from "@patternfly/react-table";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
import { FormAccess } from "../components/form-access/FormAccess";
import type { RoleFormType } from "./RealmRoleTabs";
import "./RealmRolesSection.css";
export type KeyValueType = { key: string; value: string };
type RoleAttributesProps = {
form: UseFormMethods<RoleFormType>;
form: UseFormMethods<AttributeForm>;
save: () => void;
reset: () => void;
array: {

View file

@ -46,7 +46,7 @@ const RoleLink: FunctionComponent<RoleLinkProps> = ({ children, role }) => {
const clientRouteMatch = useRouteMatch<ClientParams>(ClientRoute.path);
const to = clientRouteMatch
? toClientRole({ ...clientRouteMatch.params, id: role.id!, tab: "details" })
: toRealmRole({ realm, id: role.id! });
: toRealmRole({ realm, id: role.id!, tab: "details" });
return (
<Link key={role.id} to={to}>

View file

@ -1,5 +1,5 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react";
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";

View file

@ -1,5 +1,5 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react";
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";

View file

@ -1,9 +1,13 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react";
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
export type ClientRoleTab = "details" | "attributes" | "users-in-role";
export type ClientRoleTab =
| "details"
| "attributes"
| "users-in-role"
| "AssociateRoles";
export type ClientRoleParams = {
realm: string;

View file

@ -1,6 +1,6 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react";
import { generatePath } from "react-router";
import type { LocationDescriptorObject } from "history";
import type { RouteDef } from "../../route-config";
export type RealmRoleTab =

View file

@ -19,7 +19,7 @@ import userRoutes from "./user/routes";
export type RouteDef = {
path: string;
component: ComponentType;
component: ComponentType | React.LazyExoticComponent<() => JSX.Element>;
breadcrumb?: (t: TFunction) => string | ComponentType<any>;
access: AccessType | AccessType[];
matchOptions?: MatchOptions;

View file

@ -44,7 +44,7 @@ export const UserAttributes = ({ user }: UserAttributesProps) => {
const save = async (attributeForm: AttributeForm) => {
try {
const attributes = arrayToAttributes(attributeForm.attributes);
const attributes = arrayToAttributes(attributeForm.attributes!);
await adminClient.users.update({ id: user.id! }, { ...user, attributes });
form.setValue("attributes", convertAttributes(attributes));