Changed the idP mapper form to use the new Dynamic forms (#1641)

This commit is contained in:
Erik Jan de Wit 2021-12-14 23:46:19 +01:00 committed by GitHub
parent 920ac1dfe3
commit 67d2c8617f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 300 additions and 829 deletions

View file

@ -128,7 +128,7 @@ describe("Identity provider test", () => {
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it.skip("should edit Username Template Importer mapper", () => {
it("should edit Username Template Importer mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();

View file

@ -7,28 +7,26 @@ export default class AddMapperPage {
private mapperNameInput = "#kc-name";
private mapperRoleInput = "mapper-role-input";
private attributeName = "attribute-name";
private attributeFriendlyName = "attribute-friendly-name";
private attribute = "user.attribute";
private attributeName = "attribute.name";
private attributeFriendlyName = "attribute.friendly.name";
private attributeValue = "attribute-value";
private claimInput = "claim";
private claimValueInput = "claim-value-input";
private socialProfileJSONfieldPath = "social-profile-JSON-field-path";
private userAttribute = "user-attribute";
private userAttributeName = "user-attribute-name";
private userAttributeValue = "user-attribute-value";
private userSessionAttribute = "user-session-attribute";
private userSessionAttributeValue = "user-session-attribute-value";
private socialProfileJSONfieldPath = "jsonField";
private userAttribute = "attribute";
private userAttributeName = "userAttribute";
private userAttributeValue = "attribute.value";
private userSessionAttribute = "attribute";
private userSessionAttributeValue = "attribute.value";
private newMapperSaveButton = "new-mapper-save-button";
private regexAttributeValuesSwitch = "regex-values-switch";
private regexAttributeValuesSwitch = "are.attribute.values.regex";
private syncmodeSelectToggle = "#syncMode";
private attributesKeyInput = 'input[name="config.attributes[0].key"]';
private attributesValueInput = 'input[name="config.attributes[0].value"]';
private template = "template";
private target = "#target";
private targetDropdown = "#target-dropdown";
private selectRoleButton = "select-role-button";
private radio = "[type=radio]";
private addAssociatedRolesModalButton = "add-associated-roles-button";
goToMappersTab() {
cy.findByTestId(this.mappersTab).click();
@ -91,14 +89,16 @@ export default class AddMapperPage {
}
addRoleToMapperForm() {
const load = "/auth/admin/realms/master/roles";
cy.intercept(load).as("load");
cy.get(this.radio).eq(0).check();
cy.findByTestId(this.addAssociatedRolesModalButton).contains("Add").click();
cy.findByTestId(this.mapperRoleInput).should("have.value", "admin");
cy.get("#group-role-select-typeahead")
.click()
.get(".pf-c-select__menu-item")
.first()
.click();
cy.get("#role-role-select-typeahead")
.click()
.get(".pf-c-select__menu-item")
.first()
.click();
return this;
}
@ -115,7 +115,7 @@ export default class AddMapperPage {
cy.get(this.idpMapperSelectToggle).click();
cy.findByTestId(this.idpMapperSelect)
.contains("Advanced Attribute To Role")
.contains("Advanced Attribute to Role")
.click();
cy.get(this.attributesKeyInput).clear();
@ -126,8 +126,6 @@ export default class AddMapperPage {
this.toggleSwitch(this.regexAttributeValuesSwitch);
cy.findByTestId(this.selectRoleButton).click();
this.addRoleToMapperForm();
this.saveNewMapper();
@ -153,9 +151,7 @@ export default class AddMapperPage {
cy.findByTestId(this.template).clear();
cy.findByTestId(this.template).type("Template");
cy.get(this.target).click();
cy.get(this.targetDropdown).contains("LOCAL").click();
cy.get(this.target).click().parent().contains("BROKER_ID").click();
this.saveNewMapper();
@ -211,8 +207,7 @@ export default class AddMapperPage {
cy.findByTestId(this.attributeFriendlyName).clear();
cy.findByTestId(this.attributeFriendlyName).type("attribute friendly name");
cy.findByTestId(this.userAttributeName).clear();
cy.findByTestId(this.userAttributeName).type("user attribute name");
cy.findByTestId(this.attribute).clear().type("user attribute name");
this.saveNewMapper();
@ -234,11 +229,8 @@ export default class AddMapperPage {
.contains("Attribute Importer")
.click();
cy.findByTestId(this.claimInput).clear();
cy.findByTestId(this.claimInput).type("claim");
cy.findByTestId(this.userAttributeName).clear();
cy.findByTestId(this.userAttributeName).type("user attribute name");
cy.findByTestId(this.claimInput).clear().type("claim");
cy.findByTestId(this.attribute).clear().type("user attribute name");
this.saveNewMapper();
@ -258,9 +250,7 @@ export default class AddMapperPage {
cy.findByTestId(this.idpMapperSelect).contains("Hardcoded Role").click();
cy.findByTestId(this.mapperRoleInput).clear();
cy.findByTestId(this.mapperRoleInput).type("admin");
this.addRoleToMapperForm();
this.saveNewMapper();
return this;
@ -281,13 +271,11 @@ export default class AddMapperPage {
.contains("Hardcoded Attribute")
.click();
cy.findByTestId(this.userAttribute).clear();
cy.findByTestId(this.userAttribute).type("user session attribute");
cy.findByTestId(this.userAttribute).clear().type("user session attribute");
cy.findByTestId(this.userAttributeValue).clear();
cy.findByTestId(this.userAttributeValue).type(
"user session attribute value"
);
cy.findByTestId(this.userAttributeValue)
.clear()
.type("user session attribute value");
this.saveNewMapper();
@ -306,11 +294,10 @@ export default class AddMapperPage {
cy.get(this.idpMapperSelectToggle).click();
cy.findByTestId(this.idpMapperSelect)
.contains("SAML Attribute To Role")
.contains("SAML Attribute to Role")
.click();
cy.findByTestId(this.mapperRoleInput).clear();
cy.findByTestId(this.mapperRoleInput).type("admin");
this.addRoleToMapperForm();
this.saveNewMapper();
@ -324,9 +311,7 @@ export default class AddMapperPage {
cy.findByTestId(this.template).type("_edited");
cy.get(this.target).click();
cy.get(this.targetDropdown).contains("BROKER_ID").click();
cy.get(this.target).click().parent().contains("BROKER_USERNAME").click();
this.saveNewMapper();
@ -408,19 +393,14 @@ export default class AddMapperPage {
cy.get(this.idpMapperSelectToggle).click();
cy.findByTestId(this.idpMapperSelect).contains("Claim To Role").click();
cy.findByTestId(this.idpMapperSelect).contains("Claim to Role").click();
cy.get(this.attributesKeyInput).clear();
cy.get(this.attributesKeyInput).type("key");
cy.findByTestId("attribute-key-input").clear().type("key");
cy.findByTestId("attribute-value-input").clear().type("value");
cy.get(this.attributesValueInput).clear();
cy.get(this.attributesValueInput).type("value");
this.toggleSwitch(this.regexAttributeValuesSwitch);
cy.findByTestId(this.mapperRoleInput).clear();
cy.findByTestId(this.mapperRoleInput).type("admin");
this.toggleSwitch("are.claim.values.regex");
this.addRoleToMapperForm();
this.saveNewMapper();
return this;

View file

@ -30,8 +30,6 @@ export default {
addMapperExplain:
"If you want more fine-grain control, you can create protocol mapper on this client",
realmRoles: "Realm roles",
clientRoles: "Client roles",
selectASourceOfRoles: "Select a source of roles",
newRoleName: "New role name",
searchClientByName: "Search client by name",
clients: "Clients",
@ -50,8 +48,6 @@ export default {
predefinedMappingDescription:
"Choose one of the predefined mappings from this table",
mappingTable: "Table with predefined mapping",
roleGroup: "Use a realm role from:",
clientGroup: "Use a client role from:",
scope: "Scope",
roleMappingUpdatedSuccess: "Role mapping updated",
roleMappingUpdatedError: "Could not update role mapping {{error}}",

View file

@ -27,7 +27,7 @@ export const BooleanComponent = ({
<Controller
name={`config.${name}`}
data-testid={name}
defaultValue={defaultValue}
defaultValue={defaultValue || false}
control={control}
render={({ onChange, value }) => (
<Switch
@ -36,6 +36,7 @@ export const BooleanComponent = ({
labelOff={t("common:off")}
isChecked={value === "true" || value === true}
onChange={(value) => onChange("" + value)}
data-testid={name}
/>
)}
/>

View file

@ -0,0 +1,75 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
Button,
Chip,
ChipGroup,
FormGroup,
InputGroup,
} from "@patternfly/react-core";
import type { ComponentProps } from "./components";
import { HelpItem } from "../help-enabler/HelpItem";
import { GroupPickerDialog } from "../group/GroupPickerDialog";
export const GroupComponent = ({ name, label, helpText }: ComponentProps) => {
const { t } = useTranslation("dynamic");
const [open, setOpen] = useState(false);
const { control } = useFormContext();
return (
<Controller
name={`config.${name}`}
defaultValue=""
typeAheadAriaLabel={t("selectGroup")}
control={control}
render={({ onChange, value }) => (
<>
{open && (
<GroupPickerDialog
type="selectOne"
text={{
title: "dynamic:selectGroup",
ok: "common:select",
}}
onConfirm={(groups) => {
onChange(groups[0].path);
setOpen(false);
}}
onClose={() => setOpen(false)}
filterGroups={value}
/>
)}
<FormGroup
label={t(label!)}
labelIcon={
<HelpItem
helpText={t(helpText!)}
fieldLabelId={`dynamic:${label}`}
/>
}
fieldId={name!}
>
<InputGroup>
<ChipGroup>
{value && (
<Chip onClick={() => onChange(undefined)}>{value}</Chip>
)}
</ChipGroup>
<Button
id="kc-join-groups-button"
onClick={() => setOpen(!open)}
variant="secondary"
data-testid="join-groups-button"
>
{t("selectGroup")}
</Button>
</InputGroup>
</FormGroup>
</>
)}
/>
);
};

View file

@ -0,0 +1,23 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { FormGroup } from "@patternfly/react-core";
import type { ComponentProps } from "./components";
import { HelpItem } from "../help-enabler/HelpItem";
import { AttributeInput } from "../attribute-input/AttributeInput";
export const MapComponent = ({ name, label, helpText }: ComponentProps) => {
const { t } = useTranslation("dynamic");
return (
<FormGroup
label={t(label!)}
labelIcon={
<HelpItem helpText={t(helpText!)} fieldLabelId={`dynamic:${label}`} />
}
fieldId={name!}
>
<AttributeInput name={`config.${name}`} />
</FormGroup>
);
};

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
@ -19,12 +19,17 @@ import { useRealm } from "../../context/realm-context/RealmContext";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
const RealmClient = (realm: string): ClientRepresentation => ({
name: "realmRoles",
clientId: realm,
});
export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
const { t } = useTranslation("dynamic");
const adminClient = useAdminClient();
const { realm } = useRealm();
const { control, errors } = useFormContext();
const { control, errors, getValues } = useFormContext();
const [roleOpen, setRoleOpen] = useState(false);
const [clientsOpen, setClientsOpen] = useState(false);
@ -51,30 +56,29 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
(await adminClient.clients.listRoles({ id: client.id! })).length > 0
);
filteredClients.map(
(client) =>
(client.toString = function () {
return this.clientId!;
})
);
return filteredClients;
},
(filteredClients) => setClients(filteredClients),
[]
);
useEffect(() => {
const value = getValues(fieldName);
const [client, role] = value?.includes(".")
? value.split(".")
: ["", value || ""];
if (client) {
setSelectedClient(clients?.find((c) => c.clientId === client));
} else {
setSelectedClient(RealmClient(realm));
}
setSelectedRole({ name: role });
}, [clients, getValues]);
const createSelectGroup = (clients: ClientRepresentation[]) => {
return [
<SelectGroup key="role" label={t("roleGroup")}>
<SelectOption
key="realmRoles"
value={
{
name: "realmRoles",
toString: () => t("realmRoles"),
} as ClientRepresentation
}
>
<SelectOption key="realmRoles" value={RealmClient(realm)}>
{realm}
</SelectOption>
</SelectGroup>,
@ -133,16 +137,12 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
name={fieldName}
defaultValue=""
control={control}
rules={{ required: true }}
render={({ onChange, value }) => {
const [client, role] = value?.split(".") || ["", ""];
return (
render={({ onChange }) => (
<Split hasGutter>
<SplitItem>
{clients && (
<Select
toggleId={name!}
data-testid={name}
toggleId={`group-${name}`}
onToggle={() => setClientsOpen(!clientsOpen)}
isOpen={clientsOpen}
variant={SelectVariant.typeahead}
@ -163,9 +163,10 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
);
}
}}
selections={selectedClient || client}
selections={selectedClient?.clientId}
onClear={() => onClear(onChange)}
onSelect={(_, value) => {
onClear(onChange);
setSelectedClient(value as ClientRepresentation);
setClientsOpen(false);
}}
@ -176,6 +177,7 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
</SplitItem>
<SplitItem>
<Select
toggleId={`role-${name}`}
onToggle={(isExpanded) => setRoleOpen(isExpanded)}
isOpen={roleOpen}
variant={SelectVariant.typeahead}
@ -184,13 +186,17 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
? t("clientRoles")
: t("selectARole")
}
isDisabled={!selectedClient && !role}
isDisabled={!selectedClient}
typeAheadAriaLabel={t("selectARole")}
selections={selectedRole?.name || role}
selections={selectedRole?.name}
onSelect={(_, value) => {
const role = value as RoleRepresentation;
setSelectedRole(role);
onChange(`${selectedClient?.clientId}.${role.name}`);
onChange(
selectedClient?.name === "realmRoles"
? role.name
: `${selectedClient?.clientId}.${role.name}`
);
setRoleOpen(false);
}}
maxHeight={200}
@ -200,8 +206,7 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
</Select>
</SplitItem>
</Split>
);
}}
)}
/>
</FormGroup>
);

View file

@ -5,10 +5,12 @@ import { StringComponent } from "./StringComponent";
import { BooleanComponent } from "./BooleanComponent";
import { ListComponent } from "./ListComponent";
import { RoleComponent } from "./RoleComponent";
import { MapComponent } from "./MapComponent";
import { ScriptComponent } from "./ScriptComponent";
import { ClientSelectComponent } from "./ClientSelectComponent";
import { MultiValuedStringComponent } from "./MultivaluedStringComponent";
import { MultiValuedListComponent } from "./MultivaluedListComponent";
import { GroupComponent } from "./GroupComponent";
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
const ComponentTypes = [
@ -17,6 +19,8 @@ const ComponentTypes = [
"List",
"Role",
"Script",
"Map",
"Group",
"MultivaluedList",
"ClientList",
"MultivaluedString",
@ -32,6 +36,8 @@ export const COMPONENTS: {
List: ListComponent,
Role: RoleComponent,
Script: ScriptComponent,
Map: MapComponent,
Group: GroupComponent,
ClientList: ClientSelectComponent,
MultivaluedList: MultiValuedListComponent,
MultivaluedString: MultiValuedStringComponent,

View file

@ -2,6 +2,11 @@ export default {
dynamic: {
addMultivaluedLabel: "Add {{fieldLabel}}",
selectARole: "Select a role",
selectASourceOfRoles: "Select a source of roles",
clientRoles: "Client roles",
roleGroup: "Use a realm role from:",
clientGroup: "Use a client role from:",
selectGroup: "Select group",
usermodel: {
prop: {
label: "Property",

View file

@ -1,31 +1,23 @@
import React, { useMemo, useState } from "react";
import React, { useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { FormProvider, 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 { AttributeInput } from "../../components/attribute-input/AttributeInput";
import type { AttributeForm } from "../../components/attribute-form/AttributeForm";
import { FormAccess } from "../../components/form-access/FormAccess";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import type { IdentityProviderAddMapperParams } from "../routes/AddMapper";
import { AssociatedRolesModal } from "../../realm-roles/AssociatedRolesModal";
import type { RoleRepresentation } from "../../model/role-model";
import { useAlerts } from "../../components/alert/Alerts";
import {
@ -35,9 +27,11 @@ import {
import { convertFormValuesToObject, convertToFormValues } from "../../util";
import { toIdentityProvider } from "../routes/IdentityProvider";
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
import type { IdentityProviderMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperTypeRepresentation";
import { AddMapperForm } from "./AddMapperForm";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { groupBy } from "lodash";
import { DynamicComponents } from "../../components/dynamic/DynamicComponents";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import type { AttributeForm } from "../../components/attribute-form/AttributeForm";
export type IdPMapperRepresentationWithAttributes =
IdentityProviderMapperRepresentation & AttributeForm;
@ -49,8 +43,10 @@ export type Role = RoleRepresentation & {
export default function AddMapper() {
const { t } = useTranslation("identity-providers");
const form = useForm<IdPMapperRepresentationWithAttributes>();
const { handleSubmit, control, register, errors } = form;
const form = useForm<IdPMapperRepresentationWithAttributes>({
shouldUnregister: false,
});
const { handleSubmit, register, errors } = form;
const { addAlert, addError } = useAlerts();
const history = useHistory();
@ -60,56 +56,38 @@ export default function AddMapper() {
const { providerId, alias } = useParams<IdentityProviderAddMapperParams>();
const { id } = useParams<IdentityProviderEditMapperParams>();
const serverInfo = useServerInfo();
const identityProviders = useMemo(
() => groupBy(serverInfo.identityProviders, "groupName"),
[serverInfo]
);
const isSocialIdP = useMemo(
() =>
identityProviders["Social"]
.map((item) => item.id)
.includes(providerId.toLowerCase()),
[identityProviders, providerId]
);
const [mapperTypes, setMapperTypes] =
useState<Record<string, IdentityProviderMapperRepresentation>>();
const [mapperType, setMapperType] = useState(
isSocialIdP
? "attributeImporter"
: providerId === "saml"
? "advancedAttributeToRole"
: "hardcodedUserSessionAttribute"
);
useState<Record<string, IdentityProviderMapperTypeRepresentation>>();
const [mapperType, setMapperType] = useState<string>();
const [currentMapper, setCurrentMapper] =
useState<IdentityProviderMapperRepresentation>();
const [rolesModalOpen, setRolesModalOpen] = useState(false);
const save = async (idpMapper: IdentityProviderMapperRepresentation) => {
const attributes = JSON.stringify(idpMapper.config?.attributes ?? []);
const mapper = convertFormValuesToObject(idpMapper);
const mapper = convertFormValuesToObject(
idpMapper
) as IdentityProviderMapperRepresentation;
const attributes = JSON.stringify(idpMapper.config.attributes ?? []);
const claims = JSON.stringify(idpMapper.config.claims ?? []);
if (id) {
const updatedMapper = {
const identityProviderMapper = {
...mapper,
config: {
...mapper.config,
attributes,
claims,
},
identityProviderAlias: alias!,
id: id,
name: currentMapper?.name!,
};
if (id) {
try {
await adminClient.identityProviders.updateMapper(
{
id: id!,
alias: alias!,
},
updatedMapper
{ ...identityProviderMapper, name: currentMapper?.name! }
);
addAlert(t("mapperSaveSuccess"), AlertVariant.success);
} catch (error) {
@ -118,13 +96,7 @@ export default function AddMapper() {
} else {
try {
const createdMapper = await adminClient.identityProviders.createMapper({
identityProviderMapper: {
...mapper,
identityProviderAlias: alias,
config: {
attributes,
},
},
identityProviderMapper,
alias: alias!,
});
@ -153,6 +125,9 @@ export default function AddMapper() {
if (mapper) {
setCurrentMapper(mapper);
setupForm(mapper);
setMapperType(mapper.identityProviderMapper!);
} else {
setMapperType(Object.keys(mapperTypes)[0]);
}
setMapperTypes(mapperTypes);
@ -161,58 +136,14 @@ export default function AddMapper() {
);
const setupForm = (mapper: IdentityProviderMapperRepresentation) => {
form.reset();
convertToFormValues(mapper, form.setValue);
form.setValue("config.attributes", JSON.parse(mapper.config.attributes));
form.setValue("config.claims", JSON.parse(mapper.config.claims));
};
const targetOptions = ["local", "brokerId", "brokerUsername"];
const [targetOptionsOpen, setTargetOptionsOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<Role[]>([]);
const formValues = form.getValues();
const isSAMLAdvancedAttrToRole =
formValues.identityProviderMapper === "saml-advanced-role-idp-mapper";
const isOIDCclaimToRole =
formValues.identityProviderMapper === "oidc-role-idp-mapper";
const isOIDCAdvancedClaimToRole =
formValues.identityProviderMapper === "oidc-advanced-role-idp-mapper";
const isSAMLAttributeImporter =
formValues.identityProviderMapper === "saml-user-attribute-idp-mapper";
const isOIDCAttributeImporter =
formValues.identityProviderMapper === "oidc-user-attribute-idp-mapper";
const isHardcodedAttribute =
formValues.identityProviderMapper === "hardcoded-attribute-idp-mapper";
const isHardcodedRole =
formValues.identityProviderMapper === "oidc-hardcoded-role-idp-mapper";
const isHardcodedUserSessionAttribute =
formValues.identityProviderMapper ===
"hardcoded-user-session-attribute-idp-mapper";
const isSAMLAttributeToRole =
formValues.identityProviderMapper === "saml-role-idp-mapper";
const isSAMLUsernameTemplateImporter =
formValues.identityProviderMapper === "saml-username-idp-mapper";
const isOIDCUsernameTemplateImporter =
formValues.identityProviderMapper === "oidc-username-idp-mapper";
const isSocialAttributeImporter = useMemo(
() => formValues.identityProviderMapper?.includes("user-attribute-mapper"),
[formValues.identityProviderMapper]
);
const toggleModal = () => {
setRolesModalOpen(!rolesModalOpen);
};
if (!mapperTypes) {
return <KeycloakSpinner />;
}
return (
<PageSection variant="light">
@ -231,16 +162,6 @@ export default function AddMapper() {
}
divider
/>
{rolesModalOpen && (
<AssociatedRolesModal
id={id}
onConfirm={(role) => setSelectedRole(role)}
omitComposites
isRadio
isMapperId
toggleDialog={toggleModal}
/>
)}
<FormAccess
role="manage-identity-providers"
isHorizontal
@ -275,511 +196,15 @@ export default function AddMapper() {
id={id}
mapperTypes={mapperTypes}
updateMapperType={setMapperType}
formValues={formValues}
mapperType={mapperType}
isSocialIdP={isSocialIdP}
mapperType={mapperType!}
/>
<>
{(isSAMLAdvancedAttrToRole || isOIDCAdvancedClaimToRole) && (
<>
<FormGroup
label={
isSAMLAdvancedAttrToRole
? t("common:attributes")
: t("claims")
}
labelIcon={
<HelpItem
helpText={
isSAMLAdvancedAttrToRole
? "identity-providers-help:attributes"
: "identity-providers-help:claims"
}
fieldLabelId={
isSAMLAdvancedAttrToRole ? "attributes" : "claims"
}
/>
}
fieldId="kc-gui-order"
>
<FormProvider {...form}>
<AttributeInput name="config.attributes" />
{mapperType && mapperTypes[mapperType].properties && (
<DynamicComponents
properties={mapperTypes[mapperType].properties!}
/>
)}
</FormProvider>
</FormGroup>
<FormGroup
label={
isSAMLAdvancedAttrToRole
? t("regexAttributeValues")
: t("regexClaimValues")
}
labelIcon={
<HelpItem
helpText="identity-providers-help:regexAttributeValues"
fieldLabelId="identity-providers:regexAttributeValues"
/>
}
fieldId="regexAttributeValues"
>
<Controller
name={
isOIDCAdvancedClaimToRole
? "config.are.claim.values.regex"
: "config.are.attribute.values.regex"
}
control={control}
defaultValue="false"
render={({ onChange, value }) => (
<Switch
id="regexValues"
data-testid="regex-values-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
</FormGroup>
</>
)}
{(isSAMLUsernameTemplateImporter ||
isOIDCUsernameTemplateImporter) && (
<>
<FormGroup
label={t("template")}
labelIcon={
<HelpItem
helpText="identity-providers-help:template"
fieldLabelId="identity-providers:template"
/>
}
fieldId="kc-user-session-attribute"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
id="kc-template"
data-testid="template"
name="config.template"
defaultValue={currentMapper?.config.template}
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("target")}
labelIcon={
<HelpItem
helpText="identity-providers-help:target"
fieldLabelId="identity-providers:target"
/>
}
fieldId="kc-target"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<Controller
name="config.target"
defaultValue={currentMapper?.config.target}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="target"
datatest-id="target-select"
id="target-dropdown"
placeholderText={t("realm-settings:placeholderText")}
direction="down"
onToggle={() => setTargetOptionsOpen(!targetOptionsOpen)}
onSelect={(_, value) => {
onChange(t(`targetOptions.${value}`));
setTargetOptionsOpen(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("target")}
isOpen={targetOptionsOpen}
>
{targetOptions.map((option) => (
<SelectOption
selected={option === value}
key={option}
data-testid={option}
value={option}
>
{t(`targetOptions.${option}`)}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
</>
)}
{(isHardcodedAttribute || isHardcodedUserSessionAttribute) && (
<>
<FormGroup
label={
isHardcodedUserSessionAttribute
? t("userSessionAttribute")
: t("userAttribute")
}
labelIcon={
<HelpItem
helpText="identity-providers-help:userSessionAttribute"
fieldLabelId="identity-providers:userSessionAttribute"
/>
}
fieldId="kc-user-session-attribute"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={currentMapper?.config.attribute}
id="kc-attribute"
data-testid={
isHardcodedUserSessionAttribute
? "user-session-attribute"
: "user-attribute"
}
name="config.attribute"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={
isHardcodedUserSessionAttribute
? t("userSessionAttributeValue")
: t("userAttributeValue")
}
labelIcon={
<HelpItem
helpText="identity-providers-help:userAttributeValue"
fieldLabelId={
isHardcodedUserSessionAttribute
? "identity-providers:userSessionAttributeValue"
: "identity-providers:userAttributeValue"
}
/>
}
fieldId="kc-user-session-attribute-value"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={currentMapper?.config["attribute.value"]}
data-testid={
isHardcodedUserSessionAttribute
? "user-session-attribute-value"
: "user-attribute-value"
}
id="kc-user-session-attribute-value"
name="config.attribute.value"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
</>
)}
{(isSAMLAttributeImporter ||
isOIDCAttributeImporter ||
isOIDCclaimToRole) && (
<>
{isSAMLAttributeImporter ? (
<>
<FormGroup
label={t("mapperAttributeName")}
labelIcon={
<HelpItem
helpText="identity-providers-help:attributeName"
fieldLabelId="identity-providers:mapperAttributeName"
/>
}
fieldId="kc-attribute-name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={currentMapper?.config["attribute.name"]}
id="kc-attribute-name"
data-testid="attribute-name"
name="config.attribute.name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("mapperAttributeFriendlyName")}
labelIcon={
<HelpItem
helpText="identity-providers-help:friendlyName"
fieldLabelId="identity-providers:mapperAttributeFriendlyName"
/>
}
fieldId="kc-friendly-name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={
currentMapper?.config["attribute.friendly.name"]
}
data-testid="attribute-friendly-name"
id="kc-attribute-friendly-name"
name="config.attribute.friendly.name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
</>
) : (
<FormGroup
label={t("claim")}
labelIcon={
<HelpItem
helpText="identity-providers-help:claim"
fieldLabelId="identity-providers:claim"
/>
}
fieldId="kc-friendly-name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={currentMapper?.config["claim"]}
data-testid="claim"
id="kc-claim"
name={"config.claim"}
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
)}
<FormGroup
label={
isOIDCclaimToRole
? t("claimValue")
: t("mapperUserAttributeName")
}
labelIcon={
<HelpItem
helpText={
isOIDCclaimToRole
? "identity-providers-help:claimValue"
: "identity-providers-help:userAttributeName"
}
fieldLabelId="identity-providers:mapperUserAttributeName"
/>
}
fieldId="kc-user-attribute-name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={
isOIDCclaimToRole
? currentMapper?.config["claim.value"]
: currentMapper?.config["attribute.value"]
}
data-testid={
isOIDCclaimToRole ? "claim.value" : "user-attribute-name"
}
id={
isOIDCclaimToRole
? "kc-claim-value"
: "kc-user-attribute-name"
}
name={
isOIDCclaimToRole ? "config.claim" : "config.user.attribute"
}
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
</>
)}
{isSocialAttributeImporter && (
<>
<FormGroup
label={t("socialProfileJSONFieldPath")}
labelIcon={
<HelpItem
helpText="identity-providers-help:socialProfileJSONFieldPath"
fieldLabelId="identity-providers:socialProfileJSONFieldPath"
/>
}
fieldId="kc-social-profile-JSON-field-path"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={currentMapper?.config.attribute}
id="kc-social-profile-JSON-field-path"
data-testid={"social-profile-JSON-field-path"}
name="config.jsonField"
validated={
errors.config?.role
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("mapperUserAttributeName")}
labelIcon={
<HelpItem
helpText="identity-providers-help:socialUserAttributeName"
fieldLabelId="identity-providers:mapperUserAttributeName"
/>
}
fieldId="kc-user-session-attribute-value"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
defaultValue={currentMapper?.config.userAttribute}
data-testid={"user-attribute-name"}
id="kc-user-session-attribute-name"
name="config.userAttribute"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
</>
)}
{(isSAMLAdvancedAttrToRole ||
isHardcodedRole ||
isSAMLAttributeToRole ||
isOIDCAdvancedClaimToRole ||
isOIDCclaimToRole) && (
<FormGroup
label={t("common:role")}
labelIcon={
<HelpItem
helpText="identity-providers-help:role"
fieldLabelId="role"
/>
}
fieldId="kc-role"
validated={
errors.config?.role
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
id="kc-role"
data-testid="mapper-role-input"
name="config.role"
isDisabled={!!id}
value={
selectedRole[0]?.clientRole
? `${selectedRole[0].clientId}.${selectedRole[0]?.name}`
: selectedRole[0]?.name
}
validated={
errors.config?.role
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
<Button
data-testid="select-role-button"
onClick={() => toggleModal()}
isDisabled={!!id}
>
{t("selectRole")}
</Button>
</FormGroup>
)}
</>
<ActionGroup>
<Button
data-testid="new-mapper-save-button"

View file

@ -11,20 +11,16 @@ import {
} from "@patternfly/react-core";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import _ from "lodash";
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
import type { IdentityProviderAddMapperParams } from "../routes/AddMapper";
import { useParams } from "react-router-dom";
import type { IdPMapperRepresentationWithAttributes } from "./AddMapper";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
type AddMapperFormProps = {
mapperTypes?: Record<string, IdentityProviderMapperRepresentation>;
mapperTypes: Record<string, IdentityProviderMapperRepresentation>;
mapperType: string;
id: string;
updateMapperType: (mapperType: string) => void;
form: UseFormMethods<IdPMapperRepresentationWithAttributes>;
formValues: IdPMapperRepresentationWithAttributes;
isSocialIdP: boolean;
};
export const AddMapperForm = ({
@ -33,8 +29,6 @@ export const AddMapperForm = ({
form,
id,
updateMapperType,
formValues,
isSocialIdP,
}: AddMapperFormProps) => {
const { t } = useTranslation("identity-providers");
@ -44,7 +38,10 @@ export const AddMapperForm = ({
const syncModes = ["inherit", "import", "legacy", "force"];
const [syncModeOpen, setSyncModeOpen] = useState(false);
const { providerId } = useParams<IdentityProviderAddMapperParams>();
const serverInfo = useServerInfo();
const mapper = serverInfo.componentTypes?.[
"org.keycloak.broker.provider.IdentityProviderMapper"
].find((p) => p.id === mapperType);
return (
<>
@ -124,15 +121,7 @@ export const AddMapperForm = ({
label={t("mapperType")}
labelIcon={
<HelpItem
helpText={
formValues.identityProviderMapper ===
"saml-user-attribute-idp-mapper" &&
(providerId === "oidc" ||
providerId === "keycloak-oidc" ||
isSocialIdP)
? `identity-providers-help:oidcAttributeImporter`
: `identity-providers-help:${mapperType}`
}
helpText={mapper?.helpText}
fieldLabelId="identity-providers:mapperType"
/>
}
@ -140,13 +129,7 @@ export const AddMapperForm = ({
>
<Controller
name="identityProviderMapper"
defaultValue={
isSocialIdP
? `${providerId.toLowerCase()}-user-attribute-mapper`
: providerId === "saml"
? "saml-advanced-role-idp-mapper"
: "hardcoded-user-session-attribute-idp-mapper"
}
defaultValue={Object.keys(mapperTypes)[0]}
control={control}
render={({ onChange, value }) => (
<Select
@ -154,23 +137,13 @@ export const AddMapperForm = ({
data-testid="idp-mapper-select"
isDisabled={!!id}
required
direction="down"
onToggle={() => setMapperTypeOpen(!mapperTypeOpen)}
onSelect={(e, value) => {
const theMapper =
mapperTypes &&
Object.values(mapperTypes).find(
(item) =>
item.name?.toLowerCase() ===
value.toString().toLowerCase()
);
updateMapperType(_.camelCase(value.toString()));
onChange(theMapper?.id);
onSelect={(_, value) => {
updateMapperType(value.toString());
onChange(value.toString());
setMapperTypeOpen(false);
}}
selections={
mapperTypes &&
Object.values(mapperTypes).find(
(item) => item.id?.toLowerCase() === value
)?.name
@ -179,15 +152,14 @@ export const AddMapperForm = ({
aria-label={t("syncMode")}
isOpen={mapperTypeOpen}
>
{mapperTypes &&
Object.values(mapperTypes).map((option) => (
{Object.values(mapperTypes).map((option) => (
<SelectOption
selected={option === value}
datatest-id={option.id}
key={option.name}
value={option.name?.toUpperCase()}
value={option.id}
>
{t(`mapperTypes.${_.camelCase(option.name)}`)}
{option.name}
</SelectOption>
))}
</Select>

View file

@ -157,18 +157,6 @@ export default {
legacy: "Legacy",
force: "Force",
},
mapperTypes: {
advancedAttributeToRole: "Advanced Attribute To Role",
advancedClaimToRole: "Advanced Claim To Role",
externalRoleToRole: "External Role To Role",
claimToRole: "Claim 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",

View file

@ -27,8 +27,6 @@ export type AssociatedRolesModalProps = {
toggleDialog: () => void;
onConfirm: (newReps: RoleRepresentation[]) => void;
omitComposites?: boolean;
isRadio?: boolean;
isMapperId?: boolean;
};
type FilterType = "roles" | "clients";
@ -38,8 +36,6 @@ export const AssociatedRolesModal = ({
toggleDialog,
onConfirm,
omitComposites,
isRadio,
isMapperId,
}: AssociatedRolesModalProps) => {
const { t } = useTranslation("roles");
const [name, setName] = useState("");
@ -112,7 +108,7 @@ export const AssociatedRolesModal = ({
useFetch(
async () => {
const [role, compositeRoles] = await Promise.all([
!isMapperId ? adminClient.roles.findOneById({ id }) : undefined,
adminClient.roles.findOneById({ id }),
!omitComposites ? adminClient.roles.getCompositeRoles({ id }) : [],
]);
@ -181,7 +177,6 @@ export const AssociatedRolesModal = ({
loader={filterType === "roles" ? loader : clientRolesLoader}
ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor"
isRadio={isRadio}
isPaginated={filterType === "roles"}
isRowDisabled={(r) => compositeRoles.some((o) => o.name === r.name)}
searchTypeComponent={