Identity Providers(Mappers): Implement "Add Mapper" Screen/Functionality for OIDC and SAML IdPs (#1118)
This commit is contained in:
parent
7414e174b6
commit
a617c4ab12
14 changed files with 823 additions and 90 deletions
|
@ -7,6 +7,7 @@ import ListingPage from "../support/pages/admin_console/ListingPage";
|
|||
import CreateProviderPage from "../support/pages/admin_console/manage/identity_providers/CreateProviderPage";
|
||||
import ModalUtils from "../support/util/ModalUtils";
|
||||
import OrderDialog from "../support/pages/admin_console/manage/identity_providers/OrderDialog";
|
||||
import AddMapperPage from "../support/pages/admin_console/manage/identity_providers/AddMapperPage";
|
||||
|
||||
describe("Identity provider test", () => {
|
||||
const loginPage = new LoginPage();
|
||||
|
@ -14,7 +15,10 @@ describe("Identity provider test", () => {
|
|||
const masthead = new Masthead();
|
||||
const listingPage = new ListingPage();
|
||||
const createProviderPage = new CreateProviderPage();
|
||||
const addMapperPage = new AddMapperPage();
|
||||
|
||||
const createSuccessMsg = "Identity provider successfully created";
|
||||
const createMapperSuccessMsg = "Mapper created successfully.";
|
||||
const changeSuccessMsg =
|
||||
"Successfully changed display order of identity providers";
|
||||
const deletePrompt = "Delete provider?";
|
||||
|
@ -55,20 +59,20 @@ describe("Identity provider test", () => {
|
|||
modalUtils.checkModalTitle(deletePrompt).confirmModal();
|
||||
|
||||
masthead.checkNotificationMessage(deleteSuccessMsg);
|
||||
});
|
||||
|
||||
createProviderPage.checkGitHubCardVisible();
|
||||
it("should create facebook provider", () => {
|
||||
createProviderPage
|
||||
.clickCard("facebook")
|
||||
.fill("facebook", "123")
|
||||
.clickAdd();
|
||||
masthead.checkNotificationMessage(createSuccessMsg);
|
||||
});
|
||||
|
||||
it("should change order of providers", () => {
|
||||
const orderDialog = new OrderDialog();
|
||||
const providers = ["facebook", identityProviderName, "bitbucket"];
|
||||
|
||||
createProviderPage
|
||||
.clickCard("facebook")
|
||||
.fill("facebook", "123")
|
||||
.clickAdd();
|
||||
|
||||
cy.wait(2000);
|
||||
sidebarPage.goToIdentityProviders();
|
||||
listingPage.itemExist("facebook");
|
||||
|
||||
|
@ -125,6 +129,36 @@ describe("Identity provider test", () => {
|
|||
masthead.checkNotificationMessage(createSuccessMsg);
|
||||
});
|
||||
|
||||
it("should add facebook social mapper", () => {
|
||||
sidebarPage.goToIdentityProviders();
|
||||
|
||||
listingPage.goToItemDetails("facebook");
|
||||
|
||||
addMapperPage.goToMappersTab();
|
||||
|
||||
addMapperPage.clickAdd();
|
||||
|
||||
addMapperPage.fillSocialMapper("facebook mapper");
|
||||
|
||||
addMapperPage.saveNewMapper();
|
||||
|
||||
masthead.checkNotificationMessage(createMapperSuccessMsg);
|
||||
});
|
||||
|
||||
it("should add SAML mapper", () => {
|
||||
sidebarPage.goToIdentityProviders();
|
||||
|
||||
listingPage.goToItemDetails("saml");
|
||||
|
||||
addMapperPage.goToMappersTab();
|
||||
|
||||
addMapperPage.clickAdd();
|
||||
|
||||
addMapperPage.fillSAMLorOIDCMapper("SAML mapper");
|
||||
|
||||
masthead.checkNotificationMessage(createMapperSuccessMsg);
|
||||
});
|
||||
|
||||
it("clean up providers", () => {
|
||||
const modalUtils = new ModalUtils();
|
||||
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
export default class AddMapperPage {
|
||||
private mappersTab = "mappers-tab";
|
||||
private noMappersAddMapperButton = "no-mappers-empty-action";
|
||||
private idpMapperSelectToggle = "#identityProviderMapper";
|
||||
private idpMapperSelect = "idp-mapper-select";
|
||||
|
||||
private mapperNameInput = "#kc-name";
|
||||
private mapperRoleInput = "mapper-role-input";
|
||||
private userSessionAttribute = "user-session-attribute";
|
||||
private userSessionAttributeValue = "user-session-attribute-value";
|
||||
private newMapperSaveButton = "new-mapper-save-button";
|
||||
private regexAttributeValuesSwitch = "regex-attribute-values-switch";
|
||||
private syncmodeSelectToggle = "#syncMode";
|
||||
private attributesKeyInput = 'input[name="config.attributes[0].key"]';
|
||||
private attributesValueInput = 'input[name="config.attributes[0].value"]';
|
||||
private selectRoleButton = "select-role-button";
|
||||
private radio = "[type=radio]";
|
||||
private addAssociatedRolesModalButton = "add-associated-roles-button";
|
||||
|
||||
goToMappersTab() {
|
||||
cy.getId(this.mappersTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickAdd() {
|
||||
cy.getId(this.noMappersAddMapperButton).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickCreateDropdown() {
|
||||
cy.contains("Add provider").click();
|
||||
return this;
|
||||
}
|
||||
|
||||
saveNewMapper() {
|
||||
cy.getId(this.newMapperSaveButton).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
toggleSwitch(switchName: string) {
|
||||
cy.getId(switchName).click({ force: true });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fillSocialMapper(name: string) {
|
||||
cy.get(this.mapperNameInput).clear();
|
||||
|
||||
cy.get(this.mapperNameInput).clear().type(name);
|
||||
|
||||
cy.get(this.syncmodeSelectToggle).click();
|
||||
|
||||
cy.getId("legacy").click();
|
||||
|
||||
cy.get(this.idpMapperSelectToggle).click();
|
||||
|
||||
cy.getId(this.idpMapperSelect).contains("Attribute Importer").click();
|
||||
|
||||
cy.getId(this.userSessionAttribute).clear();
|
||||
cy.getId(this.userSessionAttribute).type("user session attribute");
|
||||
cy.getId(this.userSessionAttributeValue).clear();
|
||||
|
||||
cy.getId(this.userSessionAttributeValue).type(
|
||||
"user session attribute value"
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addRoleToMapperForm() {
|
||||
const load = "/auth/admin/realms/master/roles";
|
||||
cy.intercept(load).as("load");
|
||||
|
||||
cy.get(this.radio).eq(0).check();
|
||||
|
||||
cy.getId(this.addAssociatedRolesModalButton).contains("Add").click();
|
||||
|
||||
cy.getId(this.mapperRoleInput).should("have.value", "admin");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fillSAMLorOIDCMapper(name: string) {
|
||||
cy.get(this.mapperNameInput).clear();
|
||||
|
||||
cy.get(this.mapperNameInput).clear().type(name);
|
||||
|
||||
cy.get(this.syncmodeSelectToggle).click();
|
||||
|
||||
cy.getId("inherit").click();
|
||||
|
||||
cy.get(this.idpMapperSelectToggle).click();
|
||||
|
||||
cy.getId(this.idpMapperSelect)
|
||||
.contains("Hardcoded User Session Attribute")
|
||||
.click();
|
||||
|
||||
cy.get(this.attributesKeyInput).clear();
|
||||
cy.get(this.attributesKeyInput).type("key");
|
||||
|
||||
cy.get(this.attributesValueInput).clear();
|
||||
cy.get(this.attributesValueInput).type("value");
|
||||
|
||||
this.toggleSwitch(this.regexAttributeValuesSwitch);
|
||||
|
||||
cy.getId(this.selectRoleButton).click();
|
||||
|
||||
this.addRoleToMapperForm();
|
||||
|
||||
this.saveNewMapper();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -5,6 +5,7 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "keycloak-admin-ui",
|
||||
"version": "0.0.1",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
|
|
|
@ -24,8 +24,8 @@ export type AttributeForm = {
|
|||
|
||||
export type AttributesFormProps = {
|
||||
form: UseFormMethods<AttributeForm>;
|
||||
save: (model: AttributeForm) => void;
|
||||
reset: () => void;
|
||||
save?: (model: AttributeForm) => void;
|
||||
reset?: () => void;
|
||||
array: {
|
||||
fields: Partial<ArrayField<Record<string, any>, "id">>[];
|
||||
append: (
|
||||
|
@ -34,6 +34,7 @@ export type AttributesFormProps = {
|
|||
) => void;
|
||||
remove: (index?: number | number[] | undefined) => void;
|
||||
};
|
||||
inConfig?: boolean;
|
||||
};
|
||||
|
||||
export const arrayToAttributes = (attributeArray: KeyValueType[]) => {
|
||||
|
@ -61,12 +62,15 @@ export const AttributesForm = ({
|
|||
array: { fields, append, remove },
|
||||
reset,
|
||||
save,
|
||||
inConfig,
|
||||
}: AttributesFormProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
||||
const columns = ["Key", "Value"];
|
||||
|
||||
const watchLast = watch(`attributes[${fields.length - 1}].key`, "");
|
||||
const watchLast = inConfig
|
||||
? watch(`config.attributes[${fields.length - 1}].key`, "")
|
||||
: watch(`attributes[${fields.length - 1}].key`, "");
|
||||
|
||||
useEffect(() => {
|
||||
if (fields.length === 0) {
|
||||
|
@ -74,8 +78,13 @@ export const AttributesForm = ({
|
|||
}
|
||||
}, [fields]);
|
||||
|
||||
const noSaveCancelButtons = !save && !reset;
|
||||
|
||||
return (
|
||||
<FormAccess role="manage-realm" onSubmit={handleSubmit(save)}>
|
||||
<FormAccess
|
||||
role="manage-realm"
|
||||
onSubmit={save ? handleSubmit(save) : undefined}
|
||||
>
|
||||
<TableComposable
|
||||
className="kc-attributes__table"
|
||||
aria-label="Role attribute keys and values"
|
||||
|
@ -101,7 +110,11 @@ export const AttributesForm = ({
|
|||
dataLabel={columns[0]}
|
||||
>
|
||||
<TextInput
|
||||
name={`attributes[${rowIndex}].key`}
|
||||
name={
|
||||
inConfig
|
||||
? `config.attributes[${rowIndex}].key`
|
||||
: `attributes[${rowIndex}].key`
|
||||
}
|
||||
ref={register()}
|
||||
aria-label="key-input"
|
||||
defaultValue={attribute.key}
|
||||
|
@ -116,7 +129,11 @@ export const AttributesForm = ({
|
|||
dataLabel={columns[1]}
|
||||
>
|
||||
<TextInput
|
||||
name={`attributes[${rowIndex}].value`}
|
||||
name={
|
||||
inConfig
|
||||
? `config.attributes[${rowIndex}].value`
|
||||
: `attributes[${rowIndex}].value`
|
||||
}
|
||||
ref={register()}
|
||||
aria-label="value-input"
|
||||
defaultValue={attribute.value}
|
||||
|
@ -173,14 +190,20 @@ export const AttributesForm = ({
|
|||
</Tr>
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
<ActionGroup className="kc-attributes__action-group">
|
||||
<Button variant="primary" type="submit" isDisabled={!watchLast}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button onClick={reset} variant="link" isDisabled={!formState.isDirty}>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
{!noSaveCancelButtons && (
|
||||
<ActionGroup className="kc-attributes__action-group">
|
||||
<Button variant="primary" type="submit" isDisabled={!watchLast}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={reset}
|
||||
variant="link"
|
||||
isDisabled={!formState.isDirty}
|
||||
>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
)}
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ import { HelpItem } from "../help-enabler/HelpItem";
|
|||
|
||||
export type ViewHeaderProps = {
|
||||
titleKey: string;
|
||||
className?: string;
|
||||
badges?: ViewHeaderBadge[];
|
||||
isDropdownDisabled?: boolean;
|
||||
subKey?: string | ReactNode;
|
||||
|
@ -53,6 +54,7 @@ export type ViewHeaderBadge = {
|
|||
|
||||
export const ViewHeader = ({
|
||||
actionsDropdownId,
|
||||
className,
|
||||
titleKey,
|
||||
badges,
|
||||
isDropdownDisabled,
|
||||
|
@ -87,7 +89,9 @@ export const ViewHeader = ({
|
|||
<Level>
|
||||
<LevelItem>
|
||||
<TextContent className="pf-u-mr-sm">
|
||||
<Text component="h1">{t(titleKey)}</Text>
|
||||
<Text className={className} component="h1">
|
||||
{t(titleKey)}
|
||||
</Text>
|
||||
</TextContent>
|
||||
</LevelItem>
|
||||
{badges && (
|
||||
|
|
451
src/identity-providers/add/AddMapper.tsx
Normal file
451
src/identity-providers/add/AddMapper.tsx
Normal file
|
@ -0,0 +1,451 @@
|
|||
import React, { useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Switch,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||
import {
|
||||
AttributesForm,
|
||||
KeyValueType,
|
||||
} from "../../components/attribute-form/AttributeForm";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
|
||||
import type { IdentityProviderAddMapperParams } from "../routes/AddMapper";
|
||||
import _ from "lodash";
|
||||
import { AssociatedRolesModal } from "../../realm-roles/AssociatedRolesModal";
|
||||
import type { RoleRepresentation } from "../../model/role-model";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
|
||||
type IdPMapperRepresentationWithAttributes =
|
||||
IdentityProviderMapperRepresentation & {
|
||||
attributes: KeyValueType[];
|
||||
};
|
||||
|
||||
export const AddMapper = () => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
|
||||
const form = useForm<IdPMapperRepresentationWithAttributes>();
|
||||
const { handleSubmit, control, register, errors } = form;
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const history = useHistory();
|
||||
const { realm } = useRealm();
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
const { providerId, alias } = useParams<IdentityProviderAddMapperParams>();
|
||||
|
||||
const isSAMLorOIDC = providerId === "saml" || providerId === "oidc";
|
||||
|
||||
const [mapperTypes, setMapperTypes] =
|
||||
useState<Record<string, IdentityProviderMapperRepresentation>>();
|
||||
const [roles, setRoles] = useState<RoleRepresentation[]>([]);
|
||||
|
||||
const [rolesModalOpen, setRolesModalOpen] = useState(false);
|
||||
|
||||
const save = async (idpMapper: IdentityProviderMapperRepresentation) => {
|
||||
try {
|
||||
await adminClient.identityProviders.createMapper({
|
||||
identityProviderMapper: {
|
||||
...idpMapper,
|
||||
identityProviderAlias: alias,
|
||||
config: {
|
||||
...idpMapper.config,
|
||||
attributes: JSON.stringify(idpMapper.config.attributes),
|
||||
},
|
||||
},
|
||||
alias: alias!,
|
||||
});
|
||||
addAlert(t("mapperCreateSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError(t("mapperCreateError"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "attributes",
|
||||
});
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const allMapperTypes =
|
||||
await adminClient.identityProviders.findMapperTypes({
|
||||
alias: alias!,
|
||||
});
|
||||
|
||||
const allRoles = await adminClient.roles.find();
|
||||
return { allMapperTypes, allRoles };
|
||||
},
|
||||
({ allMapperTypes, allRoles }) => {
|
||||
setMapperTypes(allMapperTypes);
|
||||
setRoles(allRoles);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const syncModes = ["inherit", "import", "legacy", "force"];
|
||||
const [syncModeOpen, setSyncModeOpen] = useState(false);
|
||||
const [mapperTypeOpen, setMapperTypeOpen] = useState(false);
|
||||
const [selectedRole, setSelectedRole] = useState<RoleRepresentation[]>([]);
|
||||
|
||||
const toggleModal = () => {
|
||||
setRolesModalOpen(!rolesModalOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<ViewHeader
|
||||
className="kc-add-mapper-title"
|
||||
titleKey={t("addIdPMapper")}
|
||||
divider
|
||||
/>
|
||||
<AssociatedRolesModal
|
||||
onConfirm={(role) => setSelectedRole(role)}
|
||||
allRoles={roles}
|
||||
open={rolesModalOpen}
|
||||
omitComposites
|
||||
isRadio
|
||||
toggleDialog={toggleModal}
|
||||
/>
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
className="pf-u-mt-lg"
|
||||
>
|
||||
<FormGroup
|
||||
label={t("common:name")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
id="name-help-icon"
|
||||
helpText="identity-providers-help:name"
|
||||
forLabel={t("common:name")}
|
||||
forID={t(`common:helpLabel`, { label: t("common:name") })}
|
||||
/>
|
||||
}
|
||||
fieldId="kc-name"
|
||||
isRequired
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
datatest-id="name-input"
|
||||
id="kc-name"
|
||||
name="name"
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("syncModeOverride")}
|
||||
isRequired
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:syncModeOverride"
|
||||
forLabel={t("syncModeOverride")}
|
||||
forID={t(`common:helpLabel`, { label: t("syncModeOverride") })}
|
||||
/>
|
||||
}
|
||||
fieldId="syncMode"
|
||||
>
|
||||
<Controller
|
||||
name="config.syncMode"
|
||||
defaultValue={syncModes[0]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="syncMode"
|
||||
datatest-id="syncmode-select"
|
||||
required
|
||||
direction="down"
|
||||
onToggle={() => setSyncModeOpen(!syncModeOpen)}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value.toString().toUpperCase());
|
||||
setSyncModeOpen(false);
|
||||
}}
|
||||
selections={t(`syncModes.${value.toLowerCase()}`)}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("syncMode")}
|
||||
isOpen={syncModeOpen}
|
||||
>
|
||||
{syncModes.map((option) => (
|
||||
<SelectOption
|
||||
selected={option === value}
|
||||
key={option}
|
||||
data-testid={option}
|
||||
value={option.toUpperCase()}
|
||||
>
|
||||
{t(`syncModes.${option}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("mapperType")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:mapperType"
|
||||
forLabel={t("mapperType")}
|
||||
forID={t(`common:helpLabel`, { label: t("mapperType") })}
|
||||
/>
|
||||
}
|
||||
fieldId="identityProviderMapper"
|
||||
>
|
||||
<Controller
|
||||
name="identityProviderMapper"
|
||||
defaultValue={"saml-advanced-role-idp-mapper"}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="identityProviderMapper"
|
||||
data-testid="idp-mapper-select"
|
||||
required
|
||||
direction="down"
|
||||
onToggle={() => setMapperTypeOpen(!mapperTypeOpen)}
|
||||
onSelect={(e, value) => {
|
||||
const theMapper =
|
||||
mapperTypes &&
|
||||
Object.values(mapperTypes).find(
|
||||
(item) =>
|
||||
item.name?.toLowerCase() ===
|
||||
value.toString().toLowerCase()
|
||||
);
|
||||
|
||||
onChange(theMapper?.id);
|
||||
setMapperTypeOpen(false);
|
||||
}}
|
||||
selections={
|
||||
mapperTypes &&
|
||||
Object.values(mapperTypes).find(
|
||||
(item) => item.id?.toLowerCase() === value
|
||||
)?.name
|
||||
}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("syncMode")}
|
||||
isOpen={mapperTypeOpen}
|
||||
>
|
||||
{mapperTypes &&
|
||||
Object.values(mapperTypes).map((option) => (
|
||||
<SelectOption
|
||||
selected={option === value}
|
||||
datatest-id={option.id}
|
||||
key={option.name}
|
||||
value={option.name?.toUpperCase()}
|
||||
>
|
||||
{t(`mapperTypes.${_.camelCase(option.name)}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{isSAMLorOIDC ? (
|
||||
<>
|
||||
{" "}
|
||||
<FormGroup
|
||||
label={t("common:attributes")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:attributes"
|
||||
forLabel={t("attributes")}
|
||||
forID={t(`common:helpLabel`, { label: t("attributes") })}
|
||||
/>
|
||||
}
|
||||
fieldId="kc-gui-order"
|
||||
>
|
||||
<Controller
|
||||
name="config.attributes"
|
||||
defaultValue={"[]"}
|
||||
control={control}
|
||||
render={() => {
|
||||
return (
|
||||
<AttributesForm
|
||||
form={form}
|
||||
inConfig
|
||||
array={{ fields, append, remove }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("regexAttributeValues")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:regexAttributeValues"
|
||||
forLabel={t("regexAttributeValues")}
|
||||
forID={t(`common:helpLabel`, {
|
||||
label: t("regexAttributeValues"),
|
||||
})}
|
||||
/>
|
||||
}
|
||||
fieldId="regexAttributeValues"
|
||||
>
|
||||
<Controller
|
||||
name="config.are-attribute-values-regex"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="regexAttributeValues"
|
||||
data-testid="regex-attribute-values-switch"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("common:role")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
id="name-help-icon"
|
||||
helpText="identity-providers-help:role"
|
||||
forLabel={t("identity-providers-help:role")}
|
||||
forID={t(`identity-providers:helpLabel`, {
|
||||
label: t("role"),
|
||||
})}
|
||||
/>
|
||||
}
|
||||
fieldId="kc-role"
|
||||
validated={
|
||||
errors.config?.role
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
id="kc-role"
|
||||
data-testid="mapper-role-input"
|
||||
name="config.role"
|
||||
value={selectedRole[0]?.name}
|
||||
validated={
|
||||
errors.config?.role
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
data-testid="select-role-button"
|
||||
onClick={() => toggleModal()}
|
||||
>
|
||||
{t("selectRole")}
|
||||
</Button>
|
||||
</FormGroup>{" "}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("userSessionAttribute")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
id="user-session-attribute-help-icon"
|
||||
helpText="identity-providers-help:userSessionAttribute"
|
||||
forLabel={t("userSessionAttribute")}
|
||||
forID={t(`common:helpLabel`, {
|
||||
label: t("userSessionAttribute"),
|
||||
})}
|
||||
/>
|
||||
}
|
||||
fieldId="kc-user-session-attribute"
|
||||
isRequired
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
id="kc-attribute"
|
||||
data-testid="user-session-attribute"
|
||||
name="config.attribute"
|
||||
validated={
|
||||
errors.name
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("userSessionAttributeValue")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
id="user-session-attribute-value-help-icon"
|
||||
helpText="identity-providers-help:userSessionAttributeValue"
|
||||
forLabel={t("userSessionAttributeValue")}
|
||||
forID={t(`common:helpLabel`, {
|
||||
label: t("userSessionAttributeValue"),
|
||||
})}
|
||||
/>
|
||||
}
|
||||
fieldId="kc-user-session-attribute-value"
|
||||
isRequired
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
data-testid="user-session-attribute-value"
|
||||
id="kc-user-session-attribute-value"
|
||||
name="config.attribute-value"
|
||||
validated={
|
||||
errors.name
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="new-mapper-save-button"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => history.push(`/${realm}/client-scopes`)}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { Link, useHistory, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import {
|
||||
|
@ -13,6 +13,7 @@ import {
|
|||
PageSection,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
|
||||
|
@ -36,6 +37,8 @@ import { ReqAuthnConstraints } from "./ReqAuthnConstraintsSettings";
|
|||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
|
||||
import { toIdentityProviderAddMapper } from "../routes/AddMapper";
|
||||
import { toUpperCase } from "../../util";
|
||||
|
||||
type HeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
|
@ -53,12 +56,11 @@ type IdPWithMapperAttributes = IdentityProviderMapperRepresentation & {
|
|||
|
||||
const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
const { providerId, alias } =
|
||||
useParams<{ providerId: string; alias: string }>();
|
||||
const { alias } = useParams<{ alias: string }>();
|
||||
|
||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||
titleKey: "identity-providers:disableProvider",
|
||||
messageKey: t("disableConfirm", { provider: providerId }),
|
||||
messageKey: t("disableConfirm", { provider: alias }),
|
||||
continueButtonLabel: "common:disable",
|
||||
onConfirm: () => {
|
||||
onChange(!value);
|
||||
|
@ -70,7 +72,7 @@ const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
|
|||
<>
|
||||
<DisableConfirm />
|
||||
<ViewHeader
|
||||
titleKey={alias}
|
||||
titleKey={toUpperCase(alias)}
|
||||
divider={false}
|
||||
dropdownItems={[
|
||||
<DropdownItem key="delete" onClick={() => toggleDeleteDialog()}>
|
||||
|
@ -127,7 +129,7 @@ export const DetailSettings = () => {
|
|||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "identity-providers:deleteProvider",
|
||||
messageKey: t("identity-providers:deleteConfirm", { provider: providerId }),
|
||||
messageKey: t("identity-providers:deleteConfirm", { provider: alias }),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
|
@ -262,6 +264,7 @@ export const DetailSettings = () => {
|
|||
</Tab>
|
||||
<Tab
|
||||
id="mappers"
|
||||
data-testid="mappers-tab"
|
||||
eventKey="mappers"
|
||||
title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
|
||||
>
|
||||
|
@ -271,12 +274,36 @@ export const DetailSettings = () => {
|
|||
message={t("identity-providers:noMappers")}
|
||||
instructions={t("identity-providers:noMappersInstructions")}
|
||||
primaryActionText={t("identity-providers:addMapper")}
|
||||
onPrimaryAction={() =>
|
||||
history.push(
|
||||
toIdentityProviderAddMapper({
|
||||
realm,
|
||||
alias: alias!,
|
||||
providerId: providerId,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="identity-providers:mappersList"
|
||||
searchPlaceholderKey="identity-providers:searchForMapper"
|
||||
toolbarItem={
|
||||
<ToolbarItem>
|
||||
<Link
|
||||
// @ts-ignore
|
||||
to={toIdentityProviderAddMapper({
|
||||
realm,
|
||||
alias: alias!,
|
||||
providerId: providerId,
|
||||
})}
|
||||
datatest-id="add-mapper-button"
|
||||
>
|
||||
{t("addMapper")}
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
|
|
|
@ -74,29 +74,27 @@ export const SamlConnectSettings = () => {
|
|||
}, [discovering]);
|
||||
|
||||
const fileUpload = async (obj: object) => {
|
||||
if (obj) {
|
||||
const formData = new FormData();
|
||||
formData.append("providerId", id);
|
||||
formData.append("file", new Blob([JSON.stringify(obj)]));
|
||||
const formData = new FormData();
|
||||
formData.append("providerId", id);
|
||||
formData.append("file", new Blob([JSON.stringify(obj)]));
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${getBaseUrl(
|
||||
adminClient
|
||||
)}admin/realms/${realm}/identity-provider/import-config`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
Authorization: `bearer ${await adminClient.getAccessToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
setupForm(result);
|
||||
} catch (error: any) {
|
||||
setDiscoveryResult({ error });
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${getBaseUrl(
|
||||
adminClient
|
||||
)}admin/realms/${realm}/identity-provider/import-config`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
Authorization: `bearer ${await adminClient.getAccessToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
setupForm(result);
|
||||
} catch (error: any) {
|
||||
setDiscoveryResult({ error });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -116,7 +114,6 @@ export const SamlConnectSettings = () => {
|
|||
forID="kc-service-provider-entity-id"
|
||||
/>
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
|
@ -125,7 +122,7 @@ export const SamlConnectSettings = () => {
|
|||
id="kc-service-provider-entity-id"
|
||||
value={entityUrl || defaultEntityUrl}
|
||||
onChange={setEntityUrl}
|
||||
ref={register({ required: true })}
|
||||
ref={register()}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -161,7 +158,6 @@ export const SamlConnectSettings = () => {
|
|||
forID="kc-saml-entity-descriptor"
|
||||
/>
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
|
@ -170,7 +166,7 @@ export const SamlConnectSettings = () => {
|
|||
id="kc-saml-entity-descriptor"
|
||||
value={descriptorUrl}
|
||||
onChange={setDescriptorUrl}
|
||||
ref={register({ required: true })}
|
||||
ref={register()}
|
||||
validated={
|
||||
errors.samlEntityDescriptor
|
||||
? ValidatedOptions.error
|
||||
|
@ -191,7 +187,7 @@ export const SamlConnectSettings = () => {
|
|||
/>
|
||||
}
|
||||
validated={discoveryResult?.error ? "error" : "default"}
|
||||
helperTextInvalid={discoveryResult?.error?.toString()}
|
||||
helperTextInvalid={discoveryResult?.error.toString()}
|
||||
>
|
||||
<JsonFileUpload
|
||||
id="kc-import-config"
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
|
||||
.keycloak__discovery-settings__metadata .pf-c-expandable-section__toggle {
|
||||
margin-left: var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth);
|
||||
}
|
||||
|
||||
input#kc-role {
|
||||
width: 300px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
h1.kc-add-mapper-title {
|
||||
margin-left: calc(-1 * var(--pf-global--spacer--md));
|
||||
}
|
||||
|
|
|
@ -102,5 +102,17 @@ export default {
|
|||
'Specifies the comparison method used to evaluate the requested context classes or statements. The default is "Exact".',
|
||||
authnContextClassRefs: "Ordered list of requested AuthnContext ClassRefs.",
|
||||
authnContextDeclRefs: "Ordered list of requested AuthnContext DeclRefs.",
|
||||
addIdpMapperName: "Name of the mapper.",
|
||||
syncModeOverride:
|
||||
"Overrides the default sync mode of the IDP for this mapper. Values are: 'legacy' to keep the behaviour before this option was introduced, 'import' to only import the user once during first login of the user with this identity provider, 'force' to always update the user during every login with this identity provider and 'inherit' to use the sync mode defined in the identity provider for this mapper.",
|
||||
mapperType:
|
||||
"If the set of attributes exists and can be matched, grant the user the specified realm or client role.",
|
||||
attributes:
|
||||
"Name and (regex) value of the attributes to search for in token. The configured name of an attribute is searched in SAML attribute name and attribute friendly name fields. Every given attribute description must be met to set the role. If the attribute is an array, then the value must be contained in the array. If an attribute can be found several times, then one match is sufficient.",
|
||||
regexAttributeValues:
|
||||
"If enabled attribute values are interpreted as regular expressions.",
|
||||
role: "Role to grant to user if all attributes are present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference a client role the syntax is clientname.clientrole, i.e. myclient.myrole",
|
||||
userSessionAttribute: "Name of user session attribute you want to hardcode",
|
||||
userSessionAttributeValue: "Value you want to hardcode",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ export default {
|
|||
provider: "Provider details",
|
||||
addProvider: "Add provider",
|
||||
addMapper: "Add mapper",
|
||||
addIdPMapper: "Add Identity Provider Mapper",
|
||||
mappersList: "Mappers list",
|
||||
noMappers: "No Mappers",
|
||||
noMappersInstructions:
|
||||
|
@ -16,8 +17,11 @@ export default {
|
|||
addSamlProvider: "Add SAML provider",
|
||||
manageDisplayOrder: "Manage display order",
|
||||
deleteProvider: "Delete provider?",
|
||||
deleteProviderMapper: "Delete mapper?",
|
||||
deleteConfirm:
|
||||
"Are you sure you want to permanently delete the provider '{{provider}}'",
|
||||
deleteMapperConfirm:
|
||||
"Are you sure you want to permanently delete the mapper '{{mapper}}'",
|
||||
deletedSuccess: "Provider successfully deleted",
|
||||
deleteError: "Could not delete the provider {{error}}",
|
||||
disableProvider: "Disable provider?",
|
||||
|
@ -135,9 +139,27 @@ export default {
|
|||
postBrokerLoginFlowAlias: "Post login flow",
|
||||
syncMode: "Sync mode",
|
||||
syncModes: {
|
||||
inherit: "Inherit",
|
||||
import: "Import",
|
||||
legacy: "Legacy",
|
||||
force: "Force",
|
||||
},
|
||||
mapperTypes: {
|
||||
advancedAttributeToRole: "Advanced Attribute To Role",
|
||||
usernameTemplateImporter: "Username Template Importer",
|
||||
hardcodedUserSessionAttribute: "Hardcoded User Session Attribute",
|
||||
attributeImporter: "Attribute Importer",
|
||||
hardcodedRole: "Hardcoded Role",
|
||||
hardcodedAttribute: "Hardcoded Attribute",
|
||||
samlAttributeToRole: "SAML Attribute To Role",
|
||||
},
|
||||
syncModeOverride: "Sync mode override",
|
||||
mapperType: "Mapper type",
|
||||
regexAttributeValues: "Regex Attribute Values",
|
||||
selectRole: "Select role",
|
||||
mapperCreateSuccess: "Mapper created successfully.",
|
||||
mapperCreateError: "Error creating mapper.",
|
||||
userSessionAttribute: "User Session Attribute",
|
||||
userSessionAttributeValue: "User Session Attribute Value",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IdentityProviderOidcRoute } from "./routes/IdentityProviderOidc";
|
|||
import { IdentityProviderSamlRoute } from "./routes/IdentityProviderSaml";
|
||||
import { IdentityProvidersRoute } from "./routes/IdentityProviders";
|
||||
import { IdentityProviderTabRoute } from "./routes/IdentityProviderTab";
|
||||
import { IdentityProviderAddMapperRoute } from "./routes/AddMapper";
|
||||
|
||||
const routes: RouteDef[] = [
|
||||
IdentityProvidersRoute,
|
||||
|
@ -13,6 +14,7 @@ const routes: RouteDef[] = [
|
|||
IdentityProviderKeycloakOidcRoute,
|
||||
IdentityProviderRoute,
|
||||
IdentityProviderTabRoute,
|
||||
IdentityProviderAddMapperRoute,
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
|
22
src/identity-providers/routes/AddMapper.ts
Normal file
22
src/identity-providers/routes/AddMapper.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { LocationDescriptorObject } from "history";
|
||||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
import { AddMapper } from "../add/AddMapper";
|
||||
|
||||
export type IdentityProviderAddMapperParams = {
|
||||
realm: string;
|
||||
providerId: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||
export const IdentityProviderAddMapperRoute: RouteDef = {
|
||||
path: "/:realm/identity-providers/:providerId/:alias/mappers/create",
|
||||
component: AddMapper,
|
||||
access: "manage-identity-providers",
|
||||
};
|
||||
|
||||
export const toIdentityProviderAddMapper = (
|
||||
params: IdentityProviderAddMapperParams
|
||||
): LocationDescriptorObject => ({
|
||||
pathname: generatePath(IdentityProviderAddMapperRoute.path, params),
|
||||
});
|
|
@ -25,7 +25,10 @@ export type AssociatedRolesModalProps = {
|
|||
open: boolean;
|
||||
toggleDialog: () => void;
|
||||
onConfirm: (newReps: RoleRepresentation[]) => void;
|
||||
existingCompositeRoles: RoleRepresentation[];
|
||||
existingCompositeRoles?: RoleRepresentation[];
|
||||
allRoles?: RoleRepresentation[];
|
||||
omitComposites?: boolean;
|
||||
isRadio?: boolean;
|
||||
};
|
||||
|
||||
export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||
|
@ -47,33 +50,39 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
|
||||
const loader = async () => {
|
||||
const roles = await adminClient.roles.find();
|
||||
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles({
|
||||
id,
|
||||
});
|
||||
const allRoles = [...roles, ...existingAdditionalRoles];
|
||||
|
||||
const filterDupes: Role[] = allRoles.filter(
|
||||
(thing, index, self) =>
|
||||
index === self.findIndex((t) => t.name === thing.name)
|
||||
);
|
||||
if (!props.omitComposites) {
|
||||
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
|
||||
{
|
||||
id,
|
||||
}
|
||||
);
|
||||
const allRoles = [...roles, ...existingAdditionalRoles];
|
||||
|
||||
const clients = await adminClient.clients.find();
|
||||
filterDupes
|
||||
.filter((role) => role.clientRole)
|
||||
.map(
|
||||
(role) =>
|
||||
(role.clientId = clients.find(
|
||||
(client) => client.id === role.containerId
|
||||
)!.clientId!)
|
||||
const filterDupes: Role[] = allRoles.filter(
|
||||
(thing, index, self) =>
|
||||
index === self.findIndex((t) => t.name === thing.name)
|
||||
);
|
||||
|
||||
return alphabetize(filterDupes).filter((role: RoleRepresentation) => {
|
||||
return (
|
||||
props.existingCompositeRoles.find(
|
||||
(existing: RoleRepresentation) => existing.name === role.name
|
||||
) === undefined && role.name !== 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);
|
||||
};
|
||||
|
||||
const AliasRenderer = ({ id, name, clientId }: Role) => {
|
||||
|
@ -101,9 +110,6 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
});
|
||||
rolesList = [...rolesList, ...clientRolesList];
|
||||
}
|
||||
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles({
|
||||
id,
|
||||
});
|
||||
|
||||
rolesList
|
||||
.filter((role) => role.clientRole)
|
||||
|
@ -114,13 +120,23 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
)!.clientId!)
|
||||
);
|
||||
|
||||
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
||||
return (
|
||||
existingAdditionalRoles.find(
|
||||
(existing: RoleRepresentation) => existing.name === role.name
|
||||
) === undefined && role.name !== name
|
||||
if (!props.omitComposites) {
|
||||
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
|
||||
{
|
||||
id,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
||||
return (
|
||||
existingAdditionalRoles.find(
|
||||
(existing: RoleRepresentation) => existing.name === role.name
|
||||
) === undefined && role.name !== name
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return alphabetize(rolesList);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -191,6 +207,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
loader={filterType === "roles" ? loader : clientRolesLoader}
|
||||
ariaLabelKey="roles:roleList"
|
||||
searchPlaceholderKey="roles:searchFor"
|
||||
isRadio={props.isRadio}
|
||||
searchTypeComponent={
|
||||
<Dropdown
|
||||
onSelect={() => onFilterDropdownSelect(filterType)}
|
||||
|
|
Loading…
Reference in a new issue