Identity Providers(Mappers): Edit mappers (#1140)

* rebase

dont fetch rolesbyID if mapperId

save attributes

fix cypress test

cypress test updates

fix cancel route

route fix with Erik

Apply suggestions from Jon's code review

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

pr feedback from jon

remove unused import

* usefindbytestid

* PR feedback from Jon

* fix tests

* fix save bug and feedback from Jon

* remove unnecessary type

* fix cypress test
This commit is contained in:
Jenny 2021-09-22 16:27:30 -04:00 committed by GitHub
parent 809247a686
commit 83d5624bf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 395 additions and 171 deletions

View file

@ -19,6 +19,8 @@ describe("Identity provider test", () => {
const createSuccessMsg = "Identity provider successfully created"; const createSuccessMsg = "Identity provider successfully created";
const createMapperSuccessMsg = "Mapper created successfully."; const createMapperSuccessMsg = "Mapper created successfully.";
const saveMapperSuccessMsg = "Mapper saved successfully.";
const changeSuccessMsg = const changeSuccessMsg =
"Successfully changed display order of identity providers"; "Successfully changed display order of identity providers";
const deletePrompt = "Delete provider?"; const deletePrompt = "Delete provider?";
@ -53,17 +55,10 @@ describe("Identity provider test", () => {
listingPage.itemExist(identityProviderName); listingPage.itemExist(identityProviderName);
}); });
it("should delete provider", () => {
const modalUtils = new ModalUtils();
listingPage.deleteItem(identityProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
});
it("should create facebook provider", () => { it("should create facebook provider", () => {
createProviderPage createProviderPage
.clickCard("facebook") .clickCreateDropdown()
.clickItem("facebook")
.fill("facebook", "123") .fill("facebook", "123")
.clickAdd(); .clickAdd();
masthead.checkNotificationMessage(createSuccessMsg); masthead.checkNotificationMessage(createSuccessMsg);
@ -71,18 +66,11 @@ describe("Identity provider test", () => {
it("should change order of providers", () => { it("should change order of providers", () => {
const orderDialog = new OrderDialog(); const orderDialog = new OrderDialog();
const providers = ["facebook", identityProviderName, "bitbucket"]; const providers = [identityProviderName, "facebook", "bitbucket"];
sidebarPage.goToIdentityProviders(); sidebarPage.goToIdentityProviders();
listingPage.itemExist("facebook"); listingPage.itemExist("facebook");
createProviderPage
.clickCreateDropdown()
.clickItem(identityProviderName)
.fill(identityProviderName, "123")
.clickAdd();
cy.wait(2000);
sidebarPage.goToIdentityProviders(); sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName); listingPage.itemExist(identityProviderName);
@ -93,13 +81,14 @@ describe("Identity provider test", () => {
.clickAdd(); .clickAdd();
cy.wait(2000); cy.wait(2000);
sidebarPage.goToIdentityProviders(); sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName); listingPage.itemExist(identityProviderName);
orderDialog.openDialog().checkOrder(providers); orderDialog.openDialog().checkOrder(providers);
orderDialog.moveRowTo("facebook", identityProviderName); orderDialog.moveRowTo("facebook", identityProviderName);
orderDialog.checkOrder(["facebook", "bitbucket", identityProviderName]); orderDialog.checkOrder(["bitbucket", identityProviderName, "facebook"]);
orderDialog.clickSave(); orderDialog.clickSave();
masthead.checkNotificationMessage(changeSuccessMsg); masthead.checkNotificationMessage(changeSuccessMsg);
@ -129,6 +118,14 @@ describe("Identity provider test", () => {
masthead.checkNotificationMessage(createSuccessMsg); masthead.checkNotificationMessage(createSuccessMsg);
}); });
it("should delete provider", () => {
const modalUtils = new ModalUtils();
listingPage.deleteItem(identityProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
});
it("should add facebook social mapper", () => { it("should add facebook social mapper", () => {
sidebarPage.goToIdentityProviders(); sidebarPage.goToIdentityProviders();
@ -159,6 +156,32 @@ describe("Identity provider test", () => {
masthead.checkNotificationMessage(createMapperSuccessMsg); masthead.checkNotificationMessage(createMapperSuccessMsg);
}); });
it("should edit facebook mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("facebook");
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("facebook mapper");
addMapperPage.editSocialMapper();
});
it("should edit SAML mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("saml");
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("SAML mapper");
addMapperPage.editSAMLorOIDCMapper();
masthead.checkNotificationMessage(saveMapperSuccessMsg);
});
it("clean up providers", () => { it("clean up providers", () => {
const modalUtils = new ModalUtils(); const modalUtils = new ModalUtils();
@ -172,11 +195,6 @@ describe("Identity provider test", () => {
modalUtils.checkModalTitle(deletePrompt).confirmModal(); modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg); masthead.checkNotificationMessage(deleteSuccessMsg);
sidebarPage.goToIdentityProviders();
listingPage.itemExist("github").deleteItem("github");
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
sidebarPage.goToIdentityProviders(); sidebarPage.goToIdentityProviders();
listingPage.itemExist("oidc").deleteItem("oidc"); listingPage.itemExist("oidc").deleteItem("oidc");
modalUtils.checkModalTitle(deletePrompt).confirmModal(); modalUtils.checkModalTitle(deletePrompt).confirmModal();

View file

@ -113,4 +113,42 @@ export default class AddMapperPage {
return this; return this;
} }
editSocialMapper() {
cy.get(this.syncmodeSelectToggle).click();
cy.findByTestId("inherit").click();
cy.findByTestId(this.userSessionAttribute).clear();
cy.findByTestId(this.userSessionAttribute).type(
"user session attribute_edited"
);
cy.findByTestId(this.userSessionAttributeValue).clear();
cy.findByTestId(this.userSessionAttributeValue).type(
"user session attribute value_edited"
);
this.saveNewMapper();
return this;
}
editSAMLorOIDCMapper() {
cy.get(this.syncmodeSelectToggle).click();
cy.findByTestId("legacy").click();
cy.get(this.attributesKeyInput).clear();
cy.get(this.attributesKeyInput).type("key_edited");
cy.get(this.attributesValueInput).clear();
cy.get(this.attributesValueInput).type("value_edited");
this.toggleSwitch(this.regexAttributeValuesSwitch);
this.saveNewMapper();
return this;
}
} }

View file

@ -68,6 +68,8 @@ export const AttributesForm = ({
const columns = ["Key", "Value"]; const columns = ["Key", "Value"];
const noSaveCancelButtons = !save && !reset;
const watchLast = inConfig const watchLast = inConfig
? watch(`config.attributes[${fields.length - 1}].key`, "") ? watch(`config.attributes[${fields.length - 1}].key`, "")
: watch(`attributes[${fields.length - 1}].key`, ""); : watch(`attributes[${fields.length - 1}].key`, "");
@ -78,8 +80,6 @@ export const AttributesForm = ({
} }
}, [fields]); }, [fields]);
const noSaveCancelButtons = !save && !reset;
return ( return (
<FormAccess <FormAccess
role="manage-realm" role="manage-realm"

View file

@ -1,5 +1,5 @@
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import { Link, useHistory, useRouteMatch } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import _ from "lodash"; import _ from "lodash";
import { import {
@ -34,7 +34,8 @@ import { upperCaseFormatter } from "../util";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { ProviderIconMapper } from "./ProviderIconMapper"; import { ProviderIconMapper } from "./ProviderIconMapper";
import { ManageOderDialog } from "./ManageOrderDialog"; import { ManageOderDialog } from "./ManageOrderDialog";
import { toIdentityProviderTab } from "./routes/IdentityProviderTab"; import { toIdentityProvider } from "./routes/IdentityProvider";
import { toIdentityProviderCreate } from "./routes/IdentityProviderCreate";
export const IdentityProvidersSection = () => { export const IdentityProvidersSection = () => {
const { t } = useTranslation("identity-providers"); const { t } = useTranslation("identity-providers");
@ -43,7 +44,6 @@ export const IdentityProvidersSection = () => {
"groupName" "groupName"
); );
const { realm } = useRealm(); const { realm } = useRealm();
const { url } = useRouteMatch();
const history = useHistory(); const history = useHistory();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
@ -73,9 +73,9 @@ export const IdentityProvidersSection = () => {
const DetailLink = (identityProvider: IdentityProviderRepresentation) => ( const DetailLink = (identityProvider: IdentityProviderRepresentation) => (
<Link <Link
key={identityProvider.providerId} key={identityProvider.providerId}
to={toIdentityProviderTab({ to={toIdentityProvider({
realm, realm,
providerId: identityProvider.providerId, providerId: identityProvider.providerId!,
alias: identityProvider.alias!, alias: identityProvider.alias!,
tab: "settings", tab: "settings",
})} })}
@ -96,7 +96,12 @@ export const IdentityProvidersSection = () => {
); );
const navigateToCreate = (providerId: string) => const navigateToCreate = (providerId: string) =>
history.push(`${url}/${providerId}`); history.push(
toIdentityProviderCreate({
realm,
providerId,
})
);
const identityProviderOptions = () => const identityProviderOptions = () =>
Object.keys(identityProviders).map((group) => ( Object.keys(identityProviders).map((group) => (
@ -105,11 +110,18 @@ export const IdentityProvidersSection = () => {
<DropdownItem <DropdownItem
key={provider.id} key={provider.id}
value={provider.id} value={provider.id}
data-testid={provider.id} component={
onClick={() => navigateToCreate(provider.id)} <Link
> to={toIdentityProviderCreate({
{provider.name} realm,
</DropdownItem> providerId: provider.id,
})}
data-testid={provider.id}
>
{provider.name}
</Link>
}
/>
))} ))}
</DropdownGroup> </DropdownGroup>
)); ));
@ -243,7 +255,7 @@ export const IdentityProvidersSection = () => {
}, },
{ {
name: "providerId", name: "providerId",
displayKey: "identity-providers:provider", displayKey: "identity-providers:providerDetails",
cellFormatters: [upperCaseFormatter()], cellFormatters: [upperCaseFormatter()],
}, },
]} ]}

View file

@ -9,7 +9,6 @@ import {
PageSection, PageSection,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type { BreadcrumbData } from "use-react-router-breadcrumbs";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { ViewHeader } from "../../components/view-header/ViewHeader"; import { ViewHeader } from "../../components/view-header/ViewHeader";
import { toUpperCase } from "../../util"; import { toUpperCase } from "../../util";
@ -18,34 +17,12 @@ import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { GeneralSettings } from "./GeneralSettings"; import { GeneralSettings } from "./GeneralSettings";
import { toIdentityProviderTab } from "../routes/IdentityProviderTab"; import { toIdentityProvider } from "../routes/IdentityProvider";
import type { IdentityProviderCreateParams } from "../routes/IdentityProviderCreate";
export const IdentityProviderCrumb = ({ match, location }: BreadcrumbData) => {
const { t } = useTranslation();
const {
params: { id },
} = match as unknown as {
params: { [id: string]: string };
};
return (
<>
{t(
`identity-providers:${
location.pathname.endsWith("settings")
? "provider"
: "addIdentityProvider"
}`,
{
provider: toUpperCase(id),
}
)}
</>
);
};
export const AddIdentityProvider = () => { export const AddIdentityProvider = () => {
const { t } = useTranslation("identity-providers"); const { t } = useTranslation("identity-providers");
const { id } = useParams<{ id: string }>(); const { providerId } = useParams<IdentityProviderCreateParams>();
const form = useForm<IdentityProviderRepresentation>(); const form = useForm<IdentityProviderRepresentation>();
const { const {
handleSubmit, handleSubmit,
@ -61,11 +38,18 @@ export const AddIdentityProvider = () => {
try { try {
await adminClient.identityProviders.create({ await adminClient.identityProviders.create({
...provider, ...provider,
providerId: id, providerId,
alias: id, alias: providerId,
}); });
addAlert(t("createSuccess"), AlertVariant.success); addAlert(t("createSuccess"), AlertVariant.success);
history.push(toIdentityProviderTab({ realm, providerId: id, alias: id })); history.push(
toIdentityProvider({
realm,
providerId: providerId!,
alias: providerId!,
tab: "settings",
})
);
} catch (error) { } catch (error) {
addError("identity-providers:createError", error); addError("identity-providers:createError", error);
} }
@ -74,7 +58,9 @@ export const AddIdentityProvider = () => {
return ( return (
<> <>
<ViewHeader <ViewHeader
titleKey={t("addIdentityProvider", { provider: toUpperCase(id) })} titleKey={t("addIdentityProvider", {
provider: toUpperCase(providerId!),
})}
/> />
<PageSection variant="light"> <PageSection variant="light">
<FormAccess <FormAccess
@ -83,7 +69,7 @@ export const AddIdentityProvider = () => {
onSubmit={handleSubmit(save)} onSubmit={handleSubmit(save)}
> >
<FormProvider {...form}> <FormProvider {...form}>
<GeneralSettings id={id} /> <GeneralSettings id={providerId!} />
</FormProvider> </FormProvider>
<ActionGroup> <ActionGroup>
<Button <Button

View file

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useHistory, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { import {
@ -32,6 +32,9 @@ import _ from "lodash";
import { AssociatedRolesModal } from "../../realm-roles/AssociatedRolesModal"; import { AssociatedRolesModal } from "../../realm-roles/AssociatedRolesModal";
import type { RoleRepresentation } from "../../model/role-model"; import type { RoleRepresentation } from "../../model/role-model";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import type { IdentityProviderEditMapperParams } from "../routes/EditMapper";
import { convertToFormValues } from "../../util";
import { toIdentityProvider } from "../routes/IdentityProvider";
type IdPMapperRepresentationWithAttributes = type IdPMapperRepresentationWithAttributes =
IdentityProviderMapperRepresentation & { IdentityProviderMapperRepresentation & {
@ -45,61 +48,119 @@ export const AddMapper = () => {
const { handleSubmit, control, register, errors } = form; const { handleSubmit, control, register, errors } = form;
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const history = useHistory();
const { realm } = useRealm(); const { realm } = useRealm();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { providerId, alias } = useParams<IdentityProviderAddMapperParams>(); const { providerId, alias } = useParams<IdentityProviderAddMapperParams>();
const { id } = useParams<IdentityProviderEditMapperParams>();
const isSAMLorOIDC = providerId === "saml" || providerId === "oidc"; const isSAMLorOIDC = providerId === "saml" || providerId === "oidc";
const [mapperTypes, setMapperTypes] = const [mapperTypes, setMapperTypes] =
useState<Record<string, IdentityProviderMapperRepresentation>>(); useState<Record<string, IdentityProviderMapperRepresentation>>();
const [currentMapper, setCurrentMapper] =
useState<IdentityProviderMapperRepresentation>();
const [roles, setRoles] = useState<RoleRepresentation[]>([]); const [roles, setRoles] = useState<RoleRepresentation[]>([]);
const [rolesModalOpen, setRolesModalOpen] = useState(false); const [rolesModalOpen, setRolesModalOpen] = useState(false);
const save = async (idpMapper: IdentityProviderMapperRepresentation) => { const save = async (idpMapper: IdentityProviderMapperRepresentation) => {
try { if (id) {
await adminClient.identityProviders.createMapper({ const updatedMapper = {
identityProviderMapper: { ...idpMapper,
...idpMapper, identityProviderAlias: alias!,
identityProviderAlias: alias, id: id,
config: { name: currentMapper?.name!,
...idpMapper.config, config: {
attributes: JSON.stringify(idpMapper.config.attributes), ...idpMapper.config,
}, attributes: JSON.stringify(idpMapper.config?.attributes!),
}, },
alias: alias!, };
}); try {
addAlert(t("mapperCreateSuccess"), AlertVariant.success); await adminClient.identityProviders.updateMapper(
} catch (error) { {
addError(t("mapperCreateError"), error); id: id!,
alias: alias!,
},
updatedMapper
);
addAlert(t("mapperSaveSuccess"), AlertVariant.success);
} catch (error) {
addError(t("mapperSaveError"), error);
}
} else {
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({ const { append, remove, fields } = useFieldArray({
control: form.control, control: form.control,
name: "attributes", name: "config.attributes",
}); });
useFetch( useFetch(
async () => { () =>
const allMapperTypes = Promise.all([
await adminClient.identityProviders.findMapperTypes({ id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
alias: alias!, adminClient.identityProviders.findMapperTypes({ alias }),
}); !id ? adminClient.roles.find() : null,
]),
([mapper, mapperTypes, roles]) => {
if (mapper) {
setCurrentMapper(mapper);
setupForm(mapper);
}
const allRoles = await adminClient.roles.find(); setMapperTypes(mapperTypes);
return { allMapperTypes, allRoles };
}, if (roles) {
({ allMapperTypes, allRoles }) => { setRoles(roles);
setMapperTypes(allMapperTypes); }
setRoles(allRoles);
}, },
[] []
); );
const setupForm = (mapper: IdentityProviderMapperRepresentation) => {
form.reset();
Object.entries(mapper).map(([key, value]) => {
if (key === "config") {
if (mapper.config?.["are-attribute-values-regex"]) {
form.setValue(
"config.are-attribute-values-regex",
value["are-attribute-values-regex"][0]
);
}
if (mapper.config?.attributes) {
form.setValue("config.attributes", JSON.parse(value.attributes));
}
if (mapper.config?.role) {
form.setValue("config.role", value.role[0]);
}
convertToFormValues(value, "config", form.setValue);
}
form.setValue(key, value);
});
};
const syncModes = ["inherit", "import", "legacy", "force"]; const syncModes = ["inherit", "import", "legacy", "force"];
const [syncModeOpen, setSyncModeOpen] = useState(false); const [syncModeOpen, setSyncModeOpen] = useState(false);
const [mapperTypeOpen, setMapperTypeOpen] = useState(false); const [mapperTypeOpen, setMapperTypeOpen] = useState(false);
@ -113,7 +174,7 @@ export const AddMapper = () => {
<PageSection variant="light"> <PageSection variant="light">
<ViewHeader <ViewHeader
className="kc-add-mapper-title" className="kc-add-mapper-title"
titleKey={t("addIdPMapper")} titleKey={id ? t("editIdPMapper") : t("addIdPMapper")}
divider divider
/> />
<AssociatedRolesModal <AssociatedRolesModal
@ -122,6 +183,7 @@ export const AddMapper = () => {
open={rolesModalOpen} open={rolesModalOpen}
omitComposites omitComposites
isRadio isRadio
isMapperId
toggleDialog={toggleModal} toggleDialog={toggleModal}
/> />
<FormAccess <FormAccess
@ -130,6 +192,29 @@ export const AddMapper = () => {
onSubmit={handleSubmit(save)} onSubmit={handleSubmit(save)}
className="pf-u-mt-lg" className="pf-u-mt-lg"
> >
{id && (
<FormGroup
label={t("common:id")}
fieldId="kc-mapper-id"
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
value={currentMapper?.id}
datatest-id="name-input"
id="kc-name"
name="name"
isDisabled={!!id}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
)}
<FormGroup <FormGroup
label={t("common:name")} label={t("common:name")}
labelIcon={ labelIcon={
@ -153,6 +238,7 @@ export const AddMapper = () => {
datatest-id="name-input" datatest-id="name-input"
id="kc-name" id="kc-name"
name="name" name="name"
isDisabled={!!id}
validated={ validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default errors.name ? ValidatedOptions.error : ValidatedOptions.default
} }
@ -217,12 +303,17 @@ export const AddMapper = () => {
> >
<Controller <Controller
name="identityProviderMapper" name="identityProviderMapper"
defaultValue={"saml-advanced-role-idp-mapper"} defaultValue={
providerId === "saml"
? "saml-advanced-role-idp-mapper"
: "oidc-advanced-role-idp-mapper"
}
control={control} control={control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Select <Select
toggleId="identityProviderMapper" toggleId="identityProviderMapper"
data-testid="idp-mapper-select" data-testid="idp-mapper-select"
isDisabled={!!id}
required required
direction="down" direction="down"
onToggle={() => setMapperTypeOpen(!mapperTypeOpen)} onToggle={() => setMapperTypeOpen(!mapperTypeOpen)}
@ -342,7 +433,7 @@ export const AddMapper = () => {
helperTextInvalid={t("common:required")} helperTextInvalid={t("common:required")}
> >
<TextInput <TextInput
ref={register({ required: true })} ref={register()}
type="text" type="text"
id="kc-role" id="kc-role"
data-testid="mapper-role-input" data-testid="mapper-role-input"
@ -440,7 +531,17 @@ export const AddMapper = () => {
</Button> </Button>
<Button <Button
variant="link" variant="link"
onClick={() => history.push(`/${realm}/client-scopes`)} component={(props) => (
<Link
{...props}
to={toIdentityProvider({
realm,
providerId,
alias: alias!,
tab: "settings",
})}
/>
)}
> >
{t("common:cancel")} {t("common:cancel")}
</Button> </Button>

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom"; import { Link, useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form";
@ -11,6 +11,7 @@ import {
DropdownItem, DropdownItem,
Form, Form,
PageSection, PageSection,
Spinner,
Tab, Tab,
TabTitleText, TabTitleText,
ToolbarItem, ToolbarItem,
@ -38,7 +39,10 @@ import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTa
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation"; import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
import { toIdentityProviderAddMapper } from "../routes/AddMapper"; import { toIdentityProviderAddMapper } from "../routes/AddMapper";
import { toIdentityProviderEditMapper } from "../routes/EditMapper";
import { toUpperCase } from "../../util"; import { toUpperCase } from "../../util";
import type { IdentityProviderParams } from "../routes/IdentityProvider";
type HeaderProps = { type HeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -52,6 +56,7 @@ type IdPWithMapperAttributes = IdentityProviderMapperRepresentation & {
category?: string; category?: string;
helpText?: string; helpText?: string;
type: string; type: string;
mapperId: string;
}; };
const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => { const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
@ -95,21 +100,35 @@ const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
export const DetailSettings = () => { export const DetailSettings = () => {
const { t } = useTranslation("identity-providers"); const { t } = useTranslation("identity-providers");
const { providerId, alias } = const { alias, providerId } = useParams<IdentityProviderParams>();
useParams<{ providerId: string; alias: string }>();
const form = useForm<IdentityProviderRepresentation>(); const form = useForm<IdentityProviderRepresentation>();
const { handleSubmit, getValues, reset } = form; const { handleSubmit, getValues, reset } = form;
const [provider, setProvider] = useState<IdentityProviderRepresentation>();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const history = useHistory(); const history = useHistory();
const { realm } = useRealm(); const { realm } = useRealm();
const MapperLink = ({ name, mapperId }: IdPWithMapperAttributes) => (
<Link
to={toIdentityProviderEditMapper({
realm,
alias,
providerId: provider?.providerId!,
id: mapperId,
})}
>
{name}
</Link>
);
useFetch( useFetch(
() => adminClient.identityProviders.findOne({ alias: alias }), () => adminClient.identityProviders.findOne({ alias }),
(fetchedProvider) => { (fetchedProvider) => {
reset(fetchedProvider); reset(fetchedProvider);
setProvider(fetchedProvider);
}, },
[] []
); );
@ -143,10 +162,14 @@ export const DetailSettings = () => {
}, },
}); });
if (!provider) {
return <Spinner />;
}
const sections = [t("generalSettings"), t("advancedSettings")]; const sections = [t("generalSettings"), t("advancedSettings")];
const isOIDC = providerId.includes("oidc"); const isOIDC = provider.providerId!.includes("oidc");
const isSAML = providerId.includes("saml"); const isSAML = provider.providerId!.includes("saml");
const loader = async () => { const loader = async () => {
const [loaderMappers, loaderMapperTypes] = await Promise.all([ const [loaderMappers, loaderMapperTypes] = await Promise.all([
@ -164,6 +187,7 @@ export const DetailSettings = () => {
...mapperType, ...mapperType,
name: loaderMapper.name!, name: loaderMapper.name!,
type: mapperType?.name!, type: mapperType?.name!,
mapperId: loaderMapper.id!,
}; };
return result; return result;
@ -243,7 +267,7 @@ export const DetailSettings = () => {
isHorizontal isHorizontal
onSubmit={handleSubmit(save)} onSubmit={handleSubmit(save)}
> >
<AdvancedSettings isOIDC={isOIDC} isSAML={isSAML} /> <AdvancedSettings isOIDC={isOIDC!} isSAML={isSAML!} />
<ActionGroup className="keycloak__form_actions"> <ActionGroup className="keycloak__form_actions">
<Button data-testid={"save"} type="submit"> <Button data-testid={"save"} type="submit">
@ -279,7 +303,8 @@ export const DetailSettings = () => {
toIdentityProviderAddMapper({ toIdentityProviderAddMapper({
realm, realm,
alias: alias!, alias: alias!,
providerId: providerId, providerId: provider.providerId!,
tab: "mappers",
}) })
) )
} }
@ -296,7 +321,8 @@ export const DetailSettings = () => {
to={toIdentityProviderAddMapper({ to={toIdentityProviderAddMapper({
realm, realm,
alias: alias!, alias: alias!,
providerId: providerId, providerId: provider.providerId!,
tab: "mappers",
})} })}
datatest-id="add-mapper-button" datatest-id="add-mapper-button"
> >
@ -308,6 +334,7 @@ export const DetailSettings = () => {
{ {
name: "name", name: "name",
displayKey: "common:name", displayKey: "common:name",
cellRenderer: MapperLink,
}, },
{ {
name: "category", name: "category",

View file

@ -7,13 +7,13 @@ import { HelpItem } from "../../components/help-enabler/HelpItem";
import { RedirectUrl } from "../component/RedirectUrl"; import { RedirectUrl } from "../component/RedirectUrl";
import { TextField } from "../component/TextField"; import { TextField } from "../component/TextField";
import { DisplayOrder } from "../component/DisplayOrder"; import { DisplayOrder } from "../component/DisplayOrder";
import type { IdentityProviderTabParams } from "../routes/IdentityProviderTab"; import type { IdentityProviderParams } from "../routes/IdentityProvider";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
export const OIDCGeneralSettings = ({ id }: { id: string }) => { export const OIDCGeneralSettings = ({ id }: { id: string }) => {
const { t } = useTranslation("identity-providers"); const { t } = useTranslation("identity-providers");
const { t: th } = useTranslation("identity-providers-help"); const { t: th } = useTranslation("identity-providers-help");
const { tab } = useParams<IdentityProviderTabParams>(); const { tab } = useParams<IdentityProviderParams>();
const { register, errors } = useFormContext(); const { register, errors } = useFormContext();

View file

@ -8,12 +8,12 @@ import { RedirectUrl } from "../component/RedirectUrl";
import { TextField } from "../component/TextField"; import { TextField } from "../component/TextField";
import { DisplayOrder } from "../component/DisplayOrder"; import { DisplayOrder } from "../component/DisplayOrder";
import { useParams } from "react-router"; import { useParams } from "react-router";
import type { IdentityProviderTabParams } from "../routes/IdentityProviderTab"; import type { IdentityProviderParams } from "../routes/IdentityProvider";
export const SamlGeneralSettings = ({ id }: { id: string }) => { export const SamlGeneralSettings = ({ id }: { id: string }) => {
const { t } = useTranslation("identity-providers"); const { t } = useTranslation("identity-providers");
const { t: th } = useTranslation("identity-providers-help"); const { t: th } = useTranslation("identity-providers-help");
const { tab } = useParams<IdentityProviderTabParams>(); const { tab } = useParams<IdentityProviderParams>();
const { register, errors } = useFormContext(); const { register, errors } = useFormContext();

View file

@ -3,10 +3,11 @@ export default {
listExplain: listExplain:
"Through Identity Brokering it's easy to allow users to authenticate to Keycloak using external Identity Provider or Social Networks.", "Through Identity Brokering it's easy to allow users to authenticate to Keycloak using external Identity Provider or Social Networks.",
searchForProvider: "Search for provider", searchForProvider: "Search for provider",
provider: "Provider details", providerDetails: "Provider details",
addProvider: "Add provider", addProvider: "Add provider",
addMapper: "Add mapper", addMapper: "Add mapper",
addIdPMapper: "Add Identity Provider Mapper", addIdPMapper: "Add Identity Provider Mapper",
editIdPMapper: "Edit Identity Provider Mapper",
mappersList: "Mappers list", mappersList: "Mappers list",
noMappers: "No Mappers", noMappers: "No Mappers",
noMappersInstructions: noMappersInstructions:
@ -160,6 +161,8 @@ export default {
selectRole: "Select role", selectRole: "Select role",
mapperCreateSuccess: "Mapper created successfully.", mapperCreateSuccess: "Mapper created successfully.",
mapperCreateError: "Error creating mapper.", mapperCreateError: "Error creating mapper.",
mapperSaveSuccess: "Mapper saved successfully.",
mapperSaveError: "Error saving mapper: {{error}}",
userSessionAttribute: "User Session Attribute", userSessionAttribute: "User Session Attribute",
userSessionAttributeValue: "User Session Attribute Value", userSessionAttributeValue: "User Session Attribute Value",
}, },

View file

@ -4,17 +4,19 @@ import { IdentityProviderKeycloakOidcRoute } from "./routes/IdentityProviderKeyc
import { IdentityProviderOidcRoute } from "./routes/IdentityProviderOidc"; import { IdentityProviderOidcRoute } from "./routes/IdentityProviderOidc";
import { IdentityProviderSamlRoute } from "./routes/IdentityProviderSaml"; import { IdentityProviderSamlRoute } from "./routes/IdentityProviderSaml";
import { IdentityProvidersRoute } from "./routes/IdentityProviders"; import { IdentityProvidersRoute } from "./routes/IdentityProviders";
import { IdentityProviderTabRoute } from "./routes/IdentityProviderTab";
import { IdentityProviderAddMapperRoute } from "./routes/AddMapper"; import { IdentityProviderAddMapperRoute } from "./routes/AddMapper";
import { IdentityProviderEditMapperRoute } from "./routes/EditMapper";
import { IdentityProviderCreateRoute } from "./routes/IdentityProviderCreate";
const routes: RouteDef[] = [ const routes: RouteDef[] = [
IdentityProviderAddMapperRoute,
IdentityProviderEditMapperRoute,
IdentityProvidersRoute, IdentityProvidersRoute,
IdentityProviderOidcRoute, IdentityProviderOidcRoute,
IdentityProviderSamlRoute, IdentityProviderSamlRoute,
IdentityProviderKeycloakOidcRoute, IdentityProviderKeycloakOidcRoute,
IdentityProviderCreateRoute,
IdentityProviderRoute, IdentityProviderRoute,
IdentityProviderTabRoute,
IdentityProviderAddMapperRoute,
]; ];
export default routes; export default routes;

View file

@ -7,12 +7,14 @@ export type IdentityProviderAddMapperParams = {
realm: string; realm: string;
providerId: string; providerId: string;
alias: string; alias: string;
tab: string;
}; };
export const IdentityProviderAddMapperRoute: RouteDef = { export const IdentityProviderAddMapperRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId/:alias/mappers/create", path: "/:realm/identity-providers/:providerId/:alias/:tab/create",
component: AddMapper, component: AddMapper,
access: "manage-identity-providers", access: "manage-identity-providers",
breadcrumb: (t) => t("identity-providers:addIdPMapper"),
}; };
export const toIdentityProviderAddMapper = ( export const toIdentityProviderAddMapper = (

View file

@ -0,0 +1,24 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { AddMapper } from "../add/AddMapper";
export type IdentityProviderEditMapperParams = {
realm: string;
providerId: string;
alias: string;
id: string;
};
export const IdentityProviderEditMapperRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId/:alias/mappers/:id",
component: AddMapper,
access: "manage-identity-providers",
breadcrumb: (t) => t("identity-providers:editIdPMapper"),
};
export const toIdentityProviderEditMapper = (
params: IdentityProviderEditMapperParams
): LocationDescriptorObject => ({
pathname: generatePath(IdentityProviderEditMapperRoute.path, params),
});

View file

@ -1,20 +1,21 @@
import type { LocationDescriptorObject } from "history"; 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";
import { import { DetailSettings } from "../add/DetailSettings";
AddIdentityProvider,
IdentityProviderCrumb, type IdentityProviderTabs = "settings" | "mappers";
} from "../add/AddIdentityProvider";
export type IdentityProviderParams = { export type IdentityProviderParams = {
realm: string; realm: string;
id: string; providerId: string;
alias: string;
tab: IdentityProviderTabs;
}; };
export const IdentityProviderRoute: RouteDef = { export const IdentityProviderRoute: RouteDef = {
path: "/:realm/identity-providers/:id", path: "/:realm/identity-providers/:providerId/:alias/:tab",
component: AddIdentityProvider, component: DetailSettings,
breadcrumb: () => IdentityProviderCrumb, breadcrumb: (t) => t("identity-providers:providerDetails"),
access: "manage-identity-providers", access: "manage-identity-providers",
}; };

View file

@ -0,0 +1,22 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { AddIdentityProvider } from "../add/AddIdentityProvider";
export type IdentityProviderCreateParams = {
realm: string;
providerId: string;
};
export const IdentityProviderCreateRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId/add",
component: AddIdentityProvider,
breadcrumb: (t) => t("identity-providers:addProvider"),
access: "manage-identity-providers",
};
export const toIdentityProviderCreate = (
params: IdentityProviderCreateParams
): LocationDescriptorObject => ({
pathname: generatePath(IdentityProviderCreateRoute.path, params),
});

View file

@ -6,7 +6,7 @@ import { AddOpenIdConnect } from "../add/AddOpenIdConnect";
export type IdentityProviderKeycloakOidcParams = { realm: string }; export type IdentityProviderKeycloakOidcParams = { realm: string };
export const IdentityProviderKeycloakOidcRoute: RouteDef = { export const IdentityProviderKeycloakOidcRoute: RouteDef = {
path: "/:realm/identity-providers/keycloak-oidc", path: "/:realm/identity-providers/keycloak-oidc/add",
component: AddOpenIdConnect, component: AddOpenIdConnect,
breadcrumb: (t) => t("identity-providers:addKeycloakOpenIdProvider"), breadcrumb: (t) => t("identity-providers:addKeycloakOpenIdProvider"),
access: "manage-identity-providers", access: "manage-identity-providers",

View file

@ -6,7 +6,7 @@ import { AddOpenIdConnect } from "../add/AddOpenIdConnect";
export type IdentityProviderOidcParams = { realm: string }; export type IdentityProviderOidcParams = { realm: string };
export const IdentityProviderOidcRoute: RouteDef = { export const IdentityProviderOidcRoute: RouteDef = {
path: "/:realm/identity-providers/oidc", path: "/:realm/identity-providers/oidc/add",
component: AddOpenIdConnect, component: AddOpenIdConnect,
breadcrumb: (t) => t("identity-providers:addOpenIdProvider"), breadcrumb: (t) => t("identity-providers:addOpenIdProvider"),
access: "manage-identity-providers", access: "manage-identity-providers",

View file

@ -6,7 +6,7 @@ import { AddSamlConnect } from "../add/AddSamlConnect";
export type IdentityProviderSamlParams = { realm: string }; export type IdentityProviderSamlParams = { realm: string };
export const IdentityProviderSamlRoute: RouteDef = { export const IdentityProviderSamlRoute: RouteDef = {
path: "/:realm/identity-providers/saml", path: "/:realm/identity-providers/saml/add",
component: AddSamlConnect, component: AddSamlConnect,
breadcrumb: (t) => t("identity-providers:addSamlProvider"), breadcrumb: (t) => t("identity-providers:addSamlProvider"),
access: "manage-identity-providers", access: "manage-identity-providers",

View file

@ -1,25 +0,0 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { DetailSettings } from "../add/DetailSettings";
export type IdentityProviderTab = "settings";
export type IdentityProviderTabParams = {
realm: string;
providerId?: string;
alias: string;
tab?: IdentityProviderTab;
};
export const IdentityProviderTabRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId?/:alias/:tab?",
component: DetailSettings,
access: "manage-identity-providers",
};
export const toIdentityProviderTab = (
params: IdentityProviderTabParams
): LocationDescriptorObject => ({
pathname: generatePath(IdentityProviderTabRoute.path, params),
});

View file

@ -16,6 +16,7 @@ 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 _ from "lodash";
import type { RealmRoleParams } from "./routes/RealmRole";
type Role = RoleRepresentation & { type Role = RoleRepresentation & {
clientId?: string; clientId?: string;
@ -29,6 +30,7 @@ export type AssociatedRolesModalProps = {
allRoles?: RoleRepresentation[]; allRoles?: RoleRepresentation[];
omitComposites?: boolean; omitComposites?: boolean;
isRadio?: boolean; isRadio?: boolean;
isMapperId?: boolean;
}; };
export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
@ -42,7 +44,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
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<{ id: string }>(); const { id } = useParams<RealmRoleParams>();
const alphabetize = (rolesList: RoleRepresentation[]) => { const alphabetize = (rolesList: RoleRepresentation[]) => {
return _.sortBy(rolesList, (role) => role.name?.toUpperCase()); return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
@ -144,16 +146,11 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
}, [filterType]); }, [filterType]);
useFetch( useFetch(
async () => { () =>
if (id) return await adminClient.roles.findOneById({ id }); !props.isMapperId
}, ? adminClient.roles.findOneById({ id })
(fetchedRole) => { : Promise.resolve(null),
if (fetchedRole) { (role) => setName(role ? role.name! : t("createRole")),
setName(fetchedRole.name!);
} else {
setName(t("createRole"));
}
},
[] []
); );
@ -183,7 +180,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
key="add" key="add"
data-testid="add-associated-roles-button" data-testid="add-associated-roles-button"
variant="primary" variant="primary"
isDisabled={!selectedRows?.length} isDisabled={!selectedRows.length}
onClick={() => { onClick={() => {
props.toggleDialog(); props.toggleDialog();
props.onConfirm(selectedRows); props.onConfirm(selectedRows);

View file

@ -23,7 +23,7 @@ import _ from "lodash";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { UserIdpModal } from "./UserIdPModal"; import { UserIdpModal } from "./UserIdPModal";
import { toIdentityProviderTab } from "../identity-providers/routes/IdentityProviderTab"; import { toIdentityProvider } from "../identity-providers/routes/IdentityProvider";
export const UserIdentityProviderLinks = () => { export const UserIdentityProviderLinks = () => {
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
@ -42,10 +42,25 @@ export const UserIdentityProviderLinks = () => {
setIsLinkIdPModalOpen(!isLinkIdPModalOpen); setIsLinkIdPModalOpen(!isLinkIdPModalOpen);
}; };
type withProviderId = FederatedIdentityRepresentation & {
providerId: string;
};
const identityProviders = useServerInfo().identityProviders; const identityProviders = useServerInfo().identityProviders;
const getFederatedIdentities = async () => { const getFederatedIdentities = async () => {
return await adminClient.users.listFederatedIdentities({ id }); const allProviders = await adminClient.identityProviders.find();
const allFedIds = (await adminClient.users.listFederatedIdentities({
id,
})) as unknown as withProviderId[];
for (const element of allFedIds) {
element.providerId = allProviders.find(
(item) => item.alias === element.identityProvider
)?.providerId!;
}
return allFedIds;
}; };
const getAvailableIdPs = async () => { const getAvailableIdPs = async () => {
@ -89,11 +104,12 @@ export const UserIdentityProviderLinks = () => {
}, },
}); });
const idpLinkRenderer = (idp: FederatedIdentityRepresentation) => { const idpLinkRenderer = (idp: withProviderId) => {
return ( return (
<Link <Link
to={toIdentityProviderTab({ to={toIdentityProvider({
realm, realm,
providerId: idp.providerId,
alias: idp.identityProvider!, alias: idp.identityProvider!,
tab: "settings", tab: "settings",
})} })}