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:
parent
e22b27b471
commit
9c48bb4d90
17 changed files with 347 additions and 409 deletions
|
@ -12,12 +12,8 @@ export default class AssociatedRolesPage {
|
||||||
addAssociatedRealmRole() {
|
addAssociatedRealmRole() {
|
||||||
cy.findByTestId(this.actionDropdown).last().click();
|
cy.findByTestId(this.actionDropdown).last().click();
|
||||||
|
|
||||||
const load = "/auth/admin/realms/master/clients";
|
|
||||||
cy.intercept(load).as("load");
|
|
||||||
|
|
||||||
cy.findByTestId(this.addRolesDropdownItem).click();
|
cy.findByTestId(this.addRolesDropdownItem).click();
|
||||||
|
|
||||||
cy.wait(["@load"]);
|
|
||||||
cy.get(this.checkbox).eq(2).check();
|
cy.get(this.checkbox).eq(2).check();
|
||||||
|
|
||||||
cy.findByTestId(this.addAssociatedRolesModalButton).contains("Add").click();
|
cy.findByTestId(this.addAssociatedRolesModalButton).contains("Add").click();
|
||||||
|
@ -29,8 +25,6 @@ export default class AssociatedRolesPage {
|
||||||
"Composite"
|
"Composite"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.wait(["@load"]);
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,13 +60,12 @@ const SecuredRoute = ({ route }: SecuredRouteProps) => {
|
||||||
? hasAccess(...route.access)
|
? hasAccess(...route.access)
|
||||||
: hasAccess(route.access);
|
: hasAccess(route.access);
|
||||||
|
|
||||||
if (accessAllowed) {
|
if (accessAllowed)
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Spinner />}>
|
<Suspense fallback={<Spinner />}>
|
||||||
<route.component />
|
<route.component />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return <ForbiddenSection />;
|
return <ForbiddenSection />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,14 +12,15 @@ import {
|
||||||
} from "@patternfly/react-table";
|
} from "@patternfly/react-table";
|
||||||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
|
import type { RoleRepresentation } from "../../model/role-model";
|
||||||
import { FormAccess } from "../form-access/FormAccess";
|
import { FormAccess } from "../form-access/FormAccess";
|
||||||
|
|
||||||
import "./attribute-form.css";
|
import "./attribute-form.css";
|
||||||
|
|
||||||
export type KeyValueType = { key: string; value: string };
|
export type KeyValueType = { key: string; value: string };
|
||||||
|
|
||||||
export type AttributeForm = {
|
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
||||||
attributes: KeyValueType[];
|
attributes?: KeyValueType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AttributesFormProps = {
|
export type AttributesFormProps = {
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const GroupAttributes = () => {
|
||||||
const save = async (attributeForm: AttributeForm) => {
|
const save = async (attributeForm: AttributeForm) => {
|
||||||
try {
|
try {
|
||||||
const group = currentGroup();
|
const group = currentGroup();
|
||||||
const attributes = arrayToAttributes(attributeForm.attributes);
|
const attributes = arrayToAttributes(attributeForm.attributes!);
|
||||||
await adminClient.groups.update({ id: id! }, { ...group, attributes });
|
await adminClient.groups.update({ id: id! }, { ...group, attributes });
|
||||||
|
|
||||||
setSubGroups([
|
setSubGroups([
|
||||||
|
|
|
@ -21,8 +21,8 @@ import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||||
import {
|
import {
|
||||||
|
AttributeForm,
|
||||||
AttributesForm,
|
AttributesForm,
|
||||||
KeyValueType,
|
|
||||||
} from "../../components/attribute-form/AttributeForm";
|
} from "../../components/attribute-form/AttributeForm";
|
||||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
@ -39,9 +39,7 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
import { groupBy } from "lodash";
|
import { groupBy } from "lodash";
|
||||||
|
|
||||||
export type IdPMapperRepresentationWithAttributes =
|
export type IdPMapperRepresentationWithAttributes =
|
||||||
IdentityProviderMapperRepresentation & {
|
IdentityProviderMapperRepresentation & AttributeForm;
|
||||||
attributes: KeyValueType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Role = RoleRepresentation & {
|
type Role = RoleRepresentation & {
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
|
@ -86,7 +84,6 @@ export default function AddMapper() {
|
||||||
|
|
||||||
const [currentMapper, setCurrentMapper] =
|
const [currentMapper, setCurrentMapper] =
|
||||||
useState<IdentityProviderMapperRepresentation>();
|
useState<IdentityProviderMapperRepresentation>();
|
||||||
const [roles, setRoles] = useState<RoleRepresentation[]>([]);
|
|
||||||
|
|
||||||
const [rolesModalOpen, setRolesModalOpen] = useState(false);
|
const [rolesModalOpen, setRolesModalOpen] = useState(false);
|
||||||
|
|
||||||
|
@ -144,19 +141,14 @@ export default function AddMapper() {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
|
id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
|
||||||
adminClient.identityProviders.findMapperTypes({ alias }),
|
adminClient.identityProviders.findMapperTypes({ alias }),
|
||||||
!id ? adminClient.roles.find() : null,
|
|
||||||
]),
|
]),
|
||||||
([mapper, mapperTypes, roles]) => {
|
([mapper, mapperTypes]) => {
|
||||||
if (mapper) {
|
if (mapper) {
|
||||||
setCurrentMapper(mapper);
|
setCurrentMapper(mapper);
|
||||||
setupForm(mapper);
|
setupForm(mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapperTypes(mapperTypes);
|
setMapperTypes(mapperTypes);
|
||||||
|
|
||||||
if (roles) {
|
|
||||||
setRoles(roles);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
@ -257,17 +249,15 @@ export default function AddMapper() {
|
||||||
}
|
}
|
||||||
divider
|
divider
|
||||||
/>
|
/>
|
||||||
<AssociatedRolesModal
|
{rolesModalOpen && (
|
||||||
onConfirm={(role: Role[]) => {
|
<AssociatedRolesModal
|
||||||
setSelectedRole(role);
|
onConfirm={(role) => setSelectedRole(role)}
|
||||||
}}
|
omitComposites
|
||||||
allRoles={roles}
|
isRadio
|
||||||
open={rolesModalOpen}
|
isMapperId
|
||||||
omitComposites
|
toggleDialog={toggleModal}
|
||||||
isRadio
|
/>
|
||||||
isMapperId
|
)}
|
||||||
toggleDialog={toggleModal}
|
|
||||||
/>
|
|
||||||
<FormAccess
|
<FormAccess
|
||||||
role="manage-identity-providers"
|
role="manage-identity-providers"
|
||||||
isHorizontal
|
isHorizontal
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
AlertVariant,
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
@ -8,6 +9,7 @@ import {
|
||||||
Label,
|
Label,
|
||||||
Modal,
|
Modal,
|
||||||
ModalVariant,
|
ModalVariant,
|
||||||
|
Spinner,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFetch, useAdminClient } from "../context/auth/AdminClient";
|
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 { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
|
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
|
||||||
import _ from "lodash";
|
import { omit, sortBy } from "lodash";
|
||||||
import type { RealmRoleParams } from "./routes/RealmRole";
|
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 & {
|
type Role = RoleRepresentation & {
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssociatedRolesModalProps = {
|
export type AssociatedRolesModalProps = {
|
||||||
open: boolean;
|
|
||||||
toggleDialog: () => void;
|
toggleDialog: () => void;
|
||||||
onConfirm: (newReps: RoleRepresentation[]) => void;
|
onConfirm?: (newReps: RoleRepresentation[]) => void;
|
||||||
existingCompositeRoles?: RoleRepresentation[];
|
|
||||||
allRoles?: RoleRepresentation[];
|
|
||||||
omitComposites?: boolean;
|
omitComposites?: boolean;
|
||||||
isRadio?: boolean;
|
isRadio?: boolean;
|
||||||
isMapperId?: 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 { t } = useTranslation("roles");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const { realm } = useRealm();
|
||||||
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
|
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
|
||||||
|
const [compositeRoles, setCompositeRoles] = useState<RoleRepresentation[]>();
|
||||||
|
|
||||||
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
||||||
const [filterType, setFilterType] = useState("roles");
|
const [filterType, setFilterType] = useState<FilterType>("roles");
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(new Date().getTime());
|
const refresh = () => setKey(new Date().getTime());
|
||||||
|
|
||||||
const { id } = useParams<RealmRoleParams>();
|
const { id } = useParams<RealmRoleParams>();
|
||||||
|
const clientRoleRouteMatch = useRouteMatch<ClientRoleParams>(
|
||||||
|
ClientRoleRoute.path
|
||||||
|
);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const alphabetize = (rolesList: RoleRepresentation[]) => {
|
const alphabetize = (rolesList: RoleRepresentation[]) => {
|
||||||
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
|
return sortBy(rolesList, (role) => role.name?.toUpperCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async (first?: number, max?: number, search?: string) => {
|
||||||
const roles = await adminClient.roles.find();
|
const params: { [name: string]: string | number } = {
|
||||||
|
first: first!,
|
||||||
|
max: max!,
|
||||||
|
};
|
||||||
|
|
||||||
if (!props.omitComposites) {
|
const searchParam = search || "";
|
||||||
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const allRoles = [...roles, ...existingAdditionalRoles];
|
|
||||||
|
|
||||||
const filterDupes: Role[] = allRoles.filter(
|
if (searchParam) {
|
||||||
(thing, index, self) =>
|
params.search = searchParam;
|
||||||
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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return alphabetize(roles);
|
|
||||||
|
return adminClient.roles.find(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AliasRenderer = ({ id, name, clientId }: Role) => {
|
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 clientRolesLoader = async () => {
|
||||||
const clients = await adminClient.clients.find();
|
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[] = [];
|
return alphabetize(clientRoles.flat());
|
||||||
for (const id of clientIdArray) {
|
};
|
||||||
const clientRolesList = await adminClient.clients.listRoles({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
rolesList = [...rolesList, ...clientRolesList];
|
|
||||||
}
|
|
||||||
|
|
||||||
rolesList
|
const addComposites = async (composites: RoleRepresentation[]) => {
|
||||||
.filter((role) => role.clientRole)
|
const compositeArray = composites;
|
||||||
.map(
|
|
||||||
(role) =>
|
|
||||||
(role.clientId = clients.find(
|
|
||||||
(client) => client.id === role.containerId
|
|
||||||
)!.clientId!)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!props.omitComposites) {
|
const to = clientRoleRouteMatch
|
||||||
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
|
? toClientRole({ ...clientRoleRouteMatch.params, tab: "AssociateRoles" })
|
||||||
{
|
: toRealmRole({
|
||||||
|
realm,
|
||||||
id,
|
id,
|
||||||
}
|
tab: "AssociatedRoles",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminClient.roles.createComposite(
|
||||||
|
{ roleId: id, realm },
|
||||||
|
compositeArray
|
||||||
);
|
);
|
||||||
|
history.push(to);
|
||||||
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
addAlert(t("addAssociatedRolesSuccess"), AlertVariant.success);
|
||||||
return (
|
} catch (error) {
|
||||||
existingAdditionalRoles.find(
|
addError("roles:addAssociatedRolesError", error);
|
||||||
(existing: RoleRepresentation) => existing.name === role.name
|
|
||||||
) === undefined && role.name !== name
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return alphabetize(rolesList);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -146,11 +145,18 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
}, [filterType]);
|
}, [filterType]);
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
() =>
|
async () => {
|
||||||
!props.isMapperId
|
const [role, compositeRoles] = await Promise.all([
|
||||||
? adminClient.roles.findOneById({ id })
|
!isMapperId ? adminClient.roles.findOneById({ id }) : undefined,
|
||||||
: Promise.resolve(null),
|
!omitComposites ? adminClient.roles.getCompositeRoles({ id }) : [],
|
||||||
(role) => setName(role ? role.name! : t("createRole")),
|
]);
|
||||||
|
|
||||||
|
return { role, compositeRoles };
|
||||||
|
},
|
||||||
|
({ role, compositeRoles }) => {
|
||||||
|
setName(role ? role.name! : t("createRole"));
|
||||||
|
setCompositeRoles(compositeRoles);
|
||||||
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -168,12 +174,20 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
setIsFilterDropdownOpen(!isFilterDropdownOpen);
|
setIsFilterDropdownOpen(!isFilterDropdownOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!compositeRoles) {
|
||||||
|
return (
|
||||||
|
<div className="pf-u-text-align-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
data-testid="addAssociatedRole"
|
data-testid="addAssociatedRole"
|
||||||
title={t("roles:associatedRolesModalTitle", { name })}
|
title={t("roles:associatedRolesModalTitle", { name })}
|
||||||
isOpen={props.open}
|
isOpen
|
||||||
onClose={props.toggleDialog}
|
onClose={toggleDialog}
|
||||||
variant={ModalVariant.large}
|
variant={ModalVariant.large}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
|
@ -182,8 +196,12 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
variant="primary"
|
variant="primary"
|
||||||
isDisabled={!selectedRows.length}
|
isDisabled={!selectedRows.length}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.toggleDialog();
|
toggleDialog();
|
||||||
props.onConfirm(selectedRows);
|
if (onConfirm) {
|
||||||
|
onConfirm(selectedRows);
|
||||||
|
} else {
|
||||||
|
addComposites(selectedRows);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("common:add")}
|
{t("common:add")}
|
||||||
|
@ -192,7 +210,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
key="cancel"
|
key="cancel"
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.toggleDialog();
|
toggleDialog();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("common:cancel")}
|
{t("common:cancel")}
|
||||||
|
@ -204,7 +222,9 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
loader={filterType === "roles" ? loader : clientRolesLoader}
|
loader={filterType === "roles" ? loader : clientRolesLoader}
|
||||||
ariaLabelKey="roles:roleList"
|
ariaLabelKey="roles:roleList"
|
||||||
searchPlaceholderKey="roles:searchFor"
|
searchPlaceholderKey="roles:searchFor"
|
||||||
isRadio={props.isRadio}
|
isRadio={isRadio}
|
||||||
|
isPaginated={filterType === "roles"}
|
||||||
|
isRowDisabled={(r) => compositeRoles.some((o) => o.name === r.name)}
|
||||||
searchTypeComponent={
|
searchTypeComponent={
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onSelect={() => onFilterDropdownSelect(filterType)}
|
onSelect={() => onFilterDropdownSelect(filterType)}
|
||||||
|
@ -234,7 +254,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
}
|
}
|
||||||
canSelectAll
|
canSelectAll
|
||||||
onSelect={(rows) => {
|
onSelect={(rows) => {
|
||||||
setSelectedRows([...rows]);
|
setSelectedRows(rows.map((r) => omit(r, "clientId")));
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
|
@ -18,27 +18,22 @@ import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
import { emptyFormatter } from "../util";
|
import { emptyFormatter } from "../util";
|
||||||
import { AssociatedRolesModal } from "./AssociatedRolesModal";
|
import { AssociatedRolesModal } from "./AssociatedRolesModal";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
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 type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
type AssociatedRolesTabProps = {
|
type AssociatedRolesTabProps = {
|
||||||
additionalRoles: Role[];
|
parentRole: AttributeForm;
|
||||||
addComposites: (newReps: RoleRepresentation[]) => void;
|
|
||||||
parentRole: RoleFormType;
|
|
||||||
onRemove: (newReps: RoleRepresentation[]) => void;
|
|
||||||
client?: ClientRepresentation;
|
client?: ClientRepresentation;
|
||||||
|
refresh: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Role = RoleRepresentation & {
|
type Role = RoleRepresentation & {
|
||||||
clientId?: string;
|
inherited?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AssociatedRolesTab = ({
|
export const AssociatedRolesTab = ({
|
||||||
additionalRoles,
|
|
||||||
addComposites,
|
|
||||||
parentRole,
|
parentRole,
|
||||||
onRemove,
|
refresh: refreshParent,
|
||||||
}: AssociatedRolesTabProps) => {
|
}: AssociatedRolesTabProps) => {
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -49,100 +44,65 @@ export const AssociatedRolesTab = ({
|
||||||
|
|
||||||
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
|
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
|
||||||
const [isInheritedHidden, setIsInheritedHidden] = useState(false);
|
const [isInheritedHidden, setIsInheritedHidden] = useState(false);
|
||||||
const [allRoles, setAllRoles] = useState<RoleRepresentation[]>([]);
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const inheritanceMap = React.useRef<{ [key: string]: string }>({});
|
|
||||||
|
|
||||||
const getSubRoles = async (role: Role, allRoles: Role[]): Promise<Role[]> => {
|
const subRoles = async (result: Role[], roles: Role[]): Promise<Role[]> => {
|
||||||
// Fetch all composite roles
|
const promises = roles.map(async (r) => {
|
||||||
const allCompositeRoles = await adminClient.roles.getCompositeRoles({
|
if (result.find((o) => o.id === r.id)) return result;
|
||||||
id: role.id!,
|
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);
|
||||||
// Need to ensure we don't get into an infinite loop, do not add any role that is already there or the starting role
|
return [...result];
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async (first?: number, max?: number, search?: string) => {
|
||||||
const alphabetize = (rolesList: Role[]) => {
|
const compositeRoles = await adminClient.roles.getCompositeRoles({
|
||||||
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
|
id: parentRole.id!,
|
||||||
};
|
first: first,
|
||||||
const clients = await adminClient.clients.find();
|
max: max!,
|
||||||
|
search: search,
|
||||||
|
});
|
||||||
|
setCount(compositeRoles.length);
|
||||||
|
|
||||||
if (isInheritedHidden) {
|
if (!isInheritedHidden) {
|
||||||
setAllRoles(additionalRoles);
|
const children = await subRoles([], compositeRoles);
|
||||||
return alphabetize(
|
compositeRoles.splice(0, compositeRoles.length);
|
||||||
additionalRoles.filter(
|
compositeRoles.push(...children);
|
||||||
(role) =>
|
|
||||||
role.containerId === "master" && !inheritanceMap.current[role.id!]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchedRoles: Promise<Role[]> = additionalRoles.reduce(
|
await Promise.all(
|
||||||
async (acc: Promise<Role[]>, role) => {
|
compositeRoles.map(async (role) => {
|
||||||
const resolvedRoles = await acc;
|
if (role.clientRole) {
|
||||||
resolvedRoles.push(role);
|
role.containerId = (
|
||||||
const subRoles = await getSubRoles(role, resolvedRoles);
|
await adminClient.clients.findOne({
|
||||||
resolvedRoles.push(...subRoles);
|
id: role.containerId!,
|
||||||
return acc;
|
})
|
||||||
},
|
)?.clientId;
|
||||||
Promise.resolve([] as Role[])
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return fetchedRoles.then((results: Role[]) => {
|
return compositeRoles;
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const AliasRenderer = ({ id, name, clientRole, containerId }: Role) => {
|
||||||
refresh();
|
|
||||||
}, [additionalRoles, isInheritedHidden]);
|
|
||||||
|
|
||||||
const InheritedRoleName = (role: RoleRepresentation) =>
|
|
||||||
inheritanceMap.current[role.id!];
|
|
||||||
|
|
||||||
const AliasRenderer = ({ id, name, clientId }: Role) => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{clientId && (
|
{clientRole && (
|
||||||
<Label color="blue" key={`label-${id}`}>
|
<Label color="blue" key={`label-${id}`}>
|
||||||
{clientId}
|
{containerId}
|
||||||
</Label>
|
</Label>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{name}
|
{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({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
titleKey: "roles:roleRemoveAssociatedRoleConfirm",
|
titleKey: "roles:roleRemoveAssociatedRoleConfirm",
|
||||||
|
@ -163,7 +136,7 @@ export const AssociatedRolesTab = ({
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await adminClient.roles.delCompositeRoles({ id }, selectedRows);
|
await adminClient.roles.delCompositeRoles({ id }, selectedRows);
|
||||||
onRemove(selectedRows);
|
reload();
|
||||||
setSelectedRows([]);
|
setSelectedRows([]);
|
||||||
|
|
||||||
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
|
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
|
||||||
|
@ -177,21 +150,15 @@ export const AssociatedRolesTab = ({
|
||||||
useConfirmDialog({
|
useConfirmDialog({
|
||||||
titleKey: t("roles:removeAssociatedRoles") + "?",
|
titleKey: t("roles:removeAssociatedRoles") + "?",
|
||||||
messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", {
|
messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", {
|
||||||
name: parentRole?.name || t("createRole"),
|
name: parentRole.name || t("createRole"),
|
||||||
}),
|
}),
|
||||||
continueButtonLabel: "common:remove",
|
continueButtonLabel: "common:remove",
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
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);
|
await adminClient.roles.delCompositeRoles({ id }, selectedRows);
|
||||||
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
|
addAlert(t("associatedRolesRemoved"), AlertVariant.success);
|
||||||
refresh();
|
reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addError("roles:roleDeleteError", error);
|
addError("roles:roleDeleteError", error);
|
||||||
}
|
}
|
||||||
|
@ -203,20 +170,21 @@ export const AssociatedRolesTab = ({
|
||||||
<PageSection variant="light" padding={{ default: "noPadding" }}>
|
<PageSection variant="light" padding={{ default: "noPadding" }}>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
<DeleteAssociatedRolesConfirm />
|
<DeleteAssociatedRolesConfirm />
|
||||||
<AssociatedRolesModal
|
{open && <AssociatedRolesModal toggleDialog={toggleModal} />}
|
||||||
onConfirm={addComposites}
|
|
||||||
existingCompositeRoles={additionalRoles}
|
|
||||||
open={open}
|
|
||||||
toggleDialog={() => setOpen(!open)}
|
|
||||||
/>
|
|
||||||
<KeycloakDataTable
|
<KeycloakDataTable
|
||||||
key={key}
|
key={key}
|
||||||
loader={loader}
|
loader={loader}
|
||||||
ariaLabelKey="roles:roleList"
|
ariaLabelKey="roles:roleList"
|
||||||
searchPlaceholderKey="roles:searchFor"
|
searchPlaceholderKey="roles:searchFor"
|
||||||
canSelectAll
|
canSelectAll
|
||||||
|
isPaginated
|
||||||
onSelect={(rows) => {
|
onSelect={(rows) => {
|
||||||
setSelectedRows([...rows]);
|
setSelectedRows([
|
||||||
|
...rows.map((r) => {
|
||||||
|
delete r.inherited;
|
||||||
|
return r;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}}
|
}}
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
|
@ -225,7 +193,10 @@ export const AssociatedRolesTab = ({
|
||||||
label="Hide inherited roles"
|
label="Hide inherited roles"
|
||||||
key="associated-roles-check"
|
key="associated-roles-check"
|
||||||
id="kc-hide-inherited-roles-checkbox"
|
id="kc-hide-inherited-roles-checkbox"
|
||||||
onChange={() => setIsInheritedHidden(!isInheritedHidden)}
|
onChange={() => {
|
||||||
|
setIsInheritedHidden(!isInheritedHidden);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
isChecked={isInheritedHidden}
|
isChecked={isInheritedHidden}
|
||||||
/>
|
/>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
|
@ -269,9 +240,8 @@ export const AssociatedRolesTab = ({
|
||||||
cellFormatters: [emptyFormatter()],
|
cellFormatters: [emptyFormatter()],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "containerId",
|
name: "inherited",
|
||||||
displayKey: "roles:inheritedFrom",
|
displayKey: "roles:inheritedFrom",
|
||||||
cellRenderer: InheritedRoleName,
|
|
||||||
cellFormatters: [emptyFormatter()],
|
cellFormatters: [emptyFormatter()],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,17 +3,18 @@ import {
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
Button,
|
Button,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
PageSection,
|
||||||
TextArea,
|
TextArea,
|
||||||
TextInput,
|
TextInput,
|
||||||
ValidatedOptions,
|
ValidatedOptions,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { UseFormMethods } from "react-hook-form";
|
import type { UseFormMethods } from "react-hook-form";
|
||||||
import type { RoleFormType } from "./RealmRoleTabs";
|
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
|
||||||
|
|
||||||
export type RealmRoleFormProps = {
|
export type RealmRoleFormProps = {
|
||||||
form: UseFormMethods<RoleFormType>;
|
form: UseFormMethods<AttributeForm>;
|
||||||
save: () => void;
|
save: () => void;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
|
@ -28,66 +29,70 @@ export const RealmRoleForm = ({
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAccess
|
<PageSection variant="light">
|
||||||
isHorizontal
|
<FormAccess
|
||||||
onSubmit={handleSubmit(save)}
|
isHorizontal
|
||||||
role="manage-realm"
|
onSubmit={handleSubmit(save)}
|
||||||
className="pf-u-mt-lg"
|
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")}
|
|
||||||
>
|
>
|
||||||
<TextInput
|
<FormGroup
|
||||||
ref={register({ required: !editMode })}
|
label={t("roleName")}
|
||||||
type="text"
|
fieldId="kc-name"
|
||||||
id="kc-name"
|
isRequired
|
||||||
name="name"
|
validated={errors.name ? "error" : "default"}
|
||||||
isReadOnly={editMode}
|
helperTextInvalid={t("common:required")}
|
||||||
/>
|
>
|
||||||
</FormGroup>
|
<TextInput
|
||||||
<FormGroup
|
ref={register({ required: !editMode })}
|
||||||
label={t("common:description")}
|
type="text"
|
||||||
fieldId="kc-description"
|
id="kc-name"
|
||||||
validated={
|
name="name"
|
||||||
errors.description ? ValidatedOptions.error : ValidatedOptions.default
|
isReadOnly={editMode}
|
||||||
}
|
/>
|
||||||
helperTextInvalid={errors.description?.message}
|
</FormGroup>
|
||||||
>
|
<FormGroup
|
||||||
<TextArea
|
label={t("common:description")}
|
||||||
name="description"
|
fieldId="kc-description"
|
||||||
aria-label="description"
|
|
||||||
isDisabled={getValues().name?.includes("default-roles")}
|
|
||||||
ref={register({
|
|
||||||
maxLength: {
|
|
||||||
value: 255,
|
|
||||||
message: t("common:maxLength", { length: 255 }),
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
type="text"
|
|
||||||
validated={
|
validated={
|
||||||
errors.description
|
errors.description
|
||||||
? ValidatedOptions.error
|
? ValidatedOptions.error
|
||||||
: ValidatedOptions.default
|
: ValidatedOptions.default
|
||||||
}
|
}
|
||||||
id="kc-role-description"
|
helperTextInvalid={errors.description?.message}
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<ActionGroup>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={save}
|
|
||||||
data-testid="realm-roles-save-button"
|
|
||||||
>
|
>
|
||||||
{t("common:save")}
|
<TextArea
|
||||||
</Button>
|
name="description"
|
||||||
<Button onClick={() => reset()} variant="link">
|
aria-label="description"
|
||||||
{editMode ? t("common:revert") : t("common:cancel")}
|
isDisabled={getValues().name?.includes("default-roles")}
|
||||||
</Button>
|
ref={register({
|
||||||
</ActionGroup>
|
maxLength: {
|
||||||
</FormAccess>
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
|
@ -10,16 +10,16 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||||||
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||||
import type Composites from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
|
||||||
import {
|
import {
|
||||||
KeyValueType,
|
|
||||||
AttributesForm,
|
AttributesForm,
|
||||||
attributesToArray,
|
attributesToArray,
|
||||||
arrayToAttributes,
|
arrayToAttributes,
|
||||||
|
AttributeForm,
|
||||||
} from "../components/attribute-form/AttributeForm";
|
} from "../components/attribute-form/AttributeForm";
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
|
@ -31,10 +31,6 @@ import { AssociatedRolesTab } from "./AssociatedRolesTab";
|
||||||
import { UsersInRoleTab } from "./UsersInRoleTab";
|
import { UsersInRoleTab } from "./UsersInRoleTab";
|
||||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||||
|
|
||||||
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
|
|
||||||
attributes: KeyValueType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type myRealmRepresentation = RealmRepresentation & {
|
type myRealmRepresentation = RealmRepresentation & {
|
||||||
defaultRole?: {
|
defaultRole?: {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -44,11 +40,14 @@ type myRealmRepresentation = RealmRepresentation & {
|
||||||
|
|
||||||
export default function RealmRoleTabs() {
|
export default function RealmRoleTabs() {
|
||||||
const { t } = useTranslation("roles");
|
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 history = useHistory();
|
||||||
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const [role, setRole] = useState<RoleFormType>();
|
const [role, setRole] = useState<AttributeForm>();
|
||||||
|
|
||||||
const { id, clientId } = useParams<{ id: string; clientId: string }>();
|
const { id, clientId } = useParams<{ id: string; clientId: string }>();
|
||||||
|
|
||||||
|
@ -62,10 +61,6 @@ export default function RealmRoleTabs() {
|
||||||
setKey(`${new Date().getTime()}`);
|
setKey(`${new Date().getTime()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [additionalRoles, setAdditionalRoles] = useState<RoleRepresentation[]>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { addAlert, addError } = useAlerts();
|
const { addAlert, addError } = useAlerts();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
@ -80,104 +75,93 @@ export default function RealmRoleTabs() {
|
||||||
const [realm, setRealm] = useState<myRealmRepresentation>();
|
const [realm, setRealm] = useState<myRealmRepresentation>();
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
() => adminClient.realms.findOne({ realm: realmName }),
|
async () => {
|
||||||
(realm) => {
|
const realm = await adminClient.realms.findOne({ realm: realmName });
|
||||||
if (!realm) {
|
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"));
|
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);
|
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({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control: form.control,
|
control,
|
||||||
name: "attributes",
|
name: "attributes",
|
||||||
});
|
});
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
const role = form.getValues();
|
const values = getValues();
|
||||||
if (
|
if (
|
||||||
role.attributes &&
|
values.attributes &&
|
||||||
role.attributes[role.attributes.length - 1].key === ""
|
values.attributes[values.attributes.length - 1]?.key === ""
|
||||||
) {
|
) {
|
||||||
form.setValue(
|
setValue(
|
||||||
"attributes",
|
"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;
|
return;
|
||||||
}
|
}
|
||||||
const { attributes, ...rest } = role;
|
const { attributes, ...rest } = values;
|
||||||
const roleRepresentation: RoleRepresentation = rest;
|
let roleRepresentation: RoleRepresentation = rest;
|
||||||
if (id) {
|
if (id) {
|
||||||
if (attributes) {
|
if (attributes) {
|
||||||
roleRepresentation.attributes = arrayToAttributes(attributes);
|
roleRepresentation.attributes = arrayToAttributes(attributes);
|
||||||
}
|
}
|
||||||
|
roleRepresentation = {
|
||||||
|
...omit(role!, "attributes"),
|
||||||
|
...roleRepresentation,
|
||||||
|
};
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
await adminClient.roles.updateById({ id }, roleRepresentation);
|
await adminClient.roles.updateById({ id }, roleRepresentation);
|
||||||
} else {
|
} else {
|
||||||
await adminClient.clients.updateRole(
|
await adminClient.clients.updateRole(
|
||||||
{ id: clientId, roleName: role.name! },
|
{ id: clientId, roleName: values.name! },
|
||||||
roleRepresentation
|
roleRepresentation
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await adminClient.roles.createComposite(
|
setRole(convert(roleRepresentation));
|
||||||
{ roleId: id, realm: realmName },
|
|
||||||
additionalRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
setRole(role);
|
|
||||||
form.reset(role);
|
|
||||||
} else {
|
} else {
|
||||||
let createdRole;
|
let createdRole;
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
await adminClient.roles.create(roleRepresentation);
|
await adminClient.roles.create(roleRepresentation);
|
||||||
createdRole = await adminClient.roles.findOneByName({
|
createdRole = await adminClient.roles.findOneByName({
|
||||||
name: role.name!,
|
name: values.name!,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await adminClient.clients.createRole({
|
await adminClient.clients.createRole({
|
||||||
id: clientId,
|
id: clientId,
|
||||||
name: role.name,
|
name: values.name,
|
||||||
});
|
});
|
||||||
if (role.description) {
|
if (values.description) {
|
||||||
await adminClient.clients.updateRole(
|
await adminClient.clients.updateRole(
|
||||||
{ id: clientId, roleName: role.name! },
|
{ id: clientId, roleName: values.name! },
|
||||||
roleRepresentation
|
roleRepresentation
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
createdRole = await adminClient.clients.findRole({
|
createdRole = await adminClient.clients.findRole({
|
||||||
id: clientId,
|
id: clientId,
|
||||||
roleName: role.name!,
|
roleName: values.name!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!createdRole) {
|
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({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
titleKey: "roles:roleDeleteConfirm",
|
titleKey: "roles:roleDeleteConfirm",
|
||||||
messageKey: t("roles:roleDeleteConfirmDialog", {
|
messageKey: t("roles:roleDeleteConfirmDialog", {
|
||||||
|
@ -297,6 +264,9 @@ export default function RealmRoleTabs() {
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
|
const additionalRoles = await adminClient.roles.getCompositeRoles({
|
||||||
|
id: role!.id!,
|
||||||
|
});
|
||||||
await adminClient.roles.delCompositeRoles({ id }, additionalRoles);
|
await adminClient.roles.delCompositeRoles({ id }, additionalRoles);
|
||||||
addAlert(
|
addAlert(
|
||||||
t("compositeRoleOff"),
|
t("compositeRoleOff"),
|
||||||
|
@ -312,24 +282,22 @@ export default function RealmRoleTabs() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleModal = () => setOpen(!open);
|
const toggleModal = () => {
|
||||||
|
setOpen(!open);
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
<DeleteAllAssociatedRolesConfirm />
|
<DeleteAllAssociatedRolesConfirm />
|
||||||
<AssociatedRolesModal
|
{open && <AssociatedRolesModal toggleDialog={toggleModal} />}
|
||||||
onConfirm={addComposites}
|
|
||||||
existingCompositeRoles={additionalRoles}
|
|
||||||
open={open}
|
|
||||||
toggleDialog={() => setOpen(!open)}
|
|
||||||
/>
|
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey={role?.name || t("createRole")}
|
titleKey={role?.name || t("createRole")}
|
||||||
badges={[
|
badges={[
|
||||||
{
|
{
|
||||||
id: "composite-role-badge",
|
id: "composite-role-badge",
|
||||||
text: additionalRoles.length > 0 ? t("composite") : "",
|
text: role?.composite ? t("composite") : "",
|
||||||
readonly: true,
|
readonly: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
@ -340,38 +308,27 @@ export default function RealmRoleTabs() {
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light" className="pf-u-p-0">
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
{id && (
|
{id && (
|
||||||
<KeycloakTabs isBox>
|
<KeycloakTabs isBox mountOnEnter>
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="details"
|
eventKey="details"
|
||||||
title={<TabTitleText>{t("common:details")}</TabTitleText>}
|
title={<TabTitleText>{t("common:details")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<PageSection variant="light">
|
<RealmRoleForm
|
||||||
<RealmRoleForm
|
reset={() => reset(role)}
|
||||||
reset={() => form.reset(role)}
|
form={form}
|
||||||
form={form}
|
save={save}
|
||||||
save={save}
|
editMode={true}
|
||||||
editMode={true}
|
/>
|
||||||
/>
|
|
||||||
</PageSection>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
{additionalRoles.length > 0 && (
|
{role?.composite && (
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="AssociatedRoles"
|
eventKey="AssociatedRoles"
|
||||||
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
|
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<PageSection variant="light">
|
<AssociatedRolesTab parentRole={role} refresh={refresh} />
|
||||||
{role && (
|
|
||||||
<AssociatedRolesTab
|
|
||||||
additionalRoles={additionalRoles}
|
|
||||||
addComposites={addComposites}
|
|
||||||
parentRole={role}
|
|
||||||
onRemove={() => refresh()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</PageSection>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
{form.getValues().name !== realm?.defaultRole?.name && (
|
{role?.name !== realm?.defaultRole?.name && (
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="attributes"
|
eventKey="attributes"
|
||||||
className="kc-attributes-tab"
|
className="kc-attributes-tab"
|
||||||
|
@ -381,11 +338,11 @@ export default function RealmRoleTabs() {
|
||||||
form={form}
|
form={form}
|
||||||
save={save}
|
save={save}
|
||||||
array={{ fields, append, remove }}
|
array={{ fields, append, remove }}
|
||||||
reset={() => form.reset(role)}
|
reset={() => reset(role)}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
{form.getValues().name !== realm?.defaultRole?.name && (
|
{role?.name !== realm?.defaultRole?.name && (
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="users-in-role"
|
eventKey="users-in-role"
|
||||||
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
|
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
|
||||||
|
@ -396,14 +353,12 @@ export default function RealmRoleTabs() {
|
||||||
</KeycloakTabs>
|
</KeycloakTabs>
|
||||||
)}
|
)}
|
||||||
{!id && (
|
{!id && (
|
||||||
<PageSection variant="light">
|
<RealmRoleForm
|
||||||
<RealmRoleForm
|
reset={() => reset(role)}
|
||||||
reset={() => form.reset()}
|
form={form}
|
||||||
form={form}
|
save={save}
|
||||||
save={save}
|
editMode={false}
|
||||||
editMode={false}
|
/>
|
||||||
/>
|
|
||||||
</PageSection>
|
|
||||||
)}
|
)}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -12,15 +12,15 @@ import {
|
||||||
} from "@patternfly/react-table";
|
} from "@patternfly/react-table";
|
||||||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
|
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
import type { RoleFormType } from "./RealmRoleTabs";
|
|
||||||
|
|
||||||
import "./RealmRolesSection.css";
|
import "./RealmRolesSection.css";
|
||||||
|
|
||||||
export type KeyValueType = { key: string; value: string };
|
export type KeyValueType = { key: string; value: string };
|
||||||
|
|
||||||
type RoleAttributesProps = {
|
type RoleAttributesProps = {
|
||||||
form: UseFormMethods<RoleFormType>;
|
form: UseFormMethods<AttributeForm>;
|
||||||
save: () => void;
|
save: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
array: {
|
array: {
|
||||||
|
|
|
@ -46,7 +46,7 @@ const RoleLink: FunctionComponent<RoleLinkProps> = ({ children, role }) => {
|
||||||
const clientRouteMatch = useRouteMatch<ClientParams>(ClientRoute.path);
|
const clientRouteMatch = useRouteMatch<ClientParams>(ClientRoute.path);
|
||||||
const to = clientRouteMatch
|
const to = clientRouteMatch
|
||||||
? toClientRole({ ...clientRouteMatch.params, id: role.id!, tab: "details" })
|
? toClientRole({ ...clientRouteMatch.params, id: role.id!, tab: "details" })
|
||||||
: toRealmRole({ realm, id: role.id! });
|
: toRealmRole({ realm, id: role.id!, tab: "details" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={role.id} to={to}>
|
<Link key={role.id} to={to}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { LocationDescriptorObject } from "history";
|
|
||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
import { generatePath } from "react-router-dom";
|
import { generatePath } from "react-router-dom";
|
||||||
import type { RouteDef } from "../../route-config";
|
import type { RouteDef } from "../../route-config";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { LocationDescriptorObject } from "history";
|
|
||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
import { generatePath } from "react-router-dom";
|
import { generatePath } from "react-router-dom";
|
||||||
import type { RouteDef } from "../../route-config";
|
import type { RouteDef } from "../../route-config";
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import type { LocationDescriptorObject } from "history";
|
|
||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
import { generatePath } from "react-router-dom";
|
import { generatePath } from "react-router-dom";
|
||||||
import type { RouteDef } from "../../route-config";
|
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 = {
|
export type ClientRoleParams = {
|
||||||
realm: string;
|
realm: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { LocationDescriptorObject } from "history";
|
|
||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { generatePath } from "react-router";
|
import { generatePath } from "react-router";
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
import type { RouteDef } from "../../route-config";
|
import type { RouteDef } from "../../route-config";
|
||||||
|
|
||||||
export type RealmRoleTab =
|
export type RealmRoleTab =
|
||||||
|
|
|
@ -19,7 +19,7 @@ import userRoutes from "./user/routes";
|
||||||
|
|
||||||
export type RouteDef = {
|
export type RouteDef = {
|
||||||
path: string;
|
path: string;
|
||||||
component: ComponentType;
|
component: ComponentType | React.LazyExoticComponent<() => JSX.Element>;
|
||||||
breadcrumb?: (t: TFunction) => string | ComponentType<any>;
|
breadcrumb?: (t: TFunction) => string | ComponentType<any>;
|
||||||
access: AccessType | AccessType[];
|
access: AccessType | AccessType[];
|
||||||
matchOptions?: MatchOptions;
|
matchOptions?: MatchOptions;
|
||||||
|
|
|
@ -44,7 +44,7 @@ export const UserAttributes = ({ user }: UserAttributesProps) => {
|
||||||
|
|
||||||
const save = async (attributeForm: AttributeForm) => {
|
const save = async (attributeForm: AttributeForm) => {
|
||||||
try {
|
try {
|
||||||
const attributes = arrayToAttributes(attributeForm.attributes);
|
const attributes = arrayToAttributes(attributeForm.attributes!);
|
||||||
await adminClient.users.update({ id: user.id! }, { ...user, attributes });
|
await adminClient.users.update({ id: user.id! }, { ...user, attributes });
|
||||||
|
|
||||||
form.setValue("attributes", convertAttributes(attributes));
|
form.setValue("attributes", convertAttributes(attributes));
|
||||||
|
|
Loading…
Reference in a new issue