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:
parent
809247a686
commit
83d5624bf4
21 changed files with 395 additions and 171 deletions
|
@ -19,6 +19,8 @@ describe("Identity provider test", () => {
|
|||
|
||||
const createSuccessMsg = "Identity provider successfully created";
|
||||
const createMapperSuccessMsg = "Mapper created successfully.";
|
||||
const saveMapperSuccessMsg = "Mapper saved successfully.";
|
||||
|
||||
const changeSuccessMsg =
|
||||
"Successfully changed display order of identity providers";
|
||||
const deletePrompt = "Delete provider?";
|
||||
|
@ -53,17 +55,10 @@ describe("Identity provider test", () => {
|
|||
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", () => {
|
||||
createProviderPage
|
||||
.clickCard("facebook")
|
||||
.clickCreateDropdown()
|
||||
.clickItem("facebook")
|
||||
.fill("facebook", "123")
|
||||
.clickAdd();
|
||||
masthead.checkNotificationMessage(createSuccessMsg);
|
||||
|
@ -71,18 +66,11 @@ describe("Identity provider test", () => {
|
|||
|
||||
it("should change order of providers", () => {
|
||||
const orderDialog = new OrderDialog();
|
||||
const providers = ["facebook", identityProviderName, "bitbucket"];
|
||||
const providers = [identityProviderName, "facebook", "bitbucket"];
|
||||
|
||||
sidebarPage.goToIdentityProviders();
|
||||
listingPage.itemExist("facebook");
|
||||
|
||||
createProviderPage
|
||||
.clickCreateDropdown()
|
||||
.clickItem(identityProviderName)
|
||||
.fill(identityProviderName, "123")
|
||||
.clickAdd();
|
||||
|
||||
cy.wait(2000);
|
||||
sidebarPage.goToIdentityProviders();
|
||||
listingPage.itemExist(identityProviderName);
|
||||
|
||||
|
@ -93,13 +81,14 @@ describe("Identity provider test", () => {
|
|||
.clickAdd();
|
||||
|
||||
cy.wait(2000);
|
||||
|
||||
sidebarPage.goToIdentityProviders();
|
||||
listingPage.itemExist(identityProviderName);
|
||||
|
||||
orderDialog.openDialog().checkOrder(providers);
|
||||
orderDialog.moveRowTo("facebook", identityProviderName);
|
||||
|
||||
orderDialog.checkOrder(["facebook", "bitbucket", identityProviderName]);
|
||||
orderDialog.checkOrder(["bitbucket", identityProviderName, "facebook"]);
|
||||
|
||||
orderDialog.clickSave();
|
||||
masthead.checkNotificationMessage(changeSuccessMsg);
|
||||
|
@ -129,6 +118,14 @@ describe("Identity provider test", () => {
|
|||
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", () => {
|
||||
sidebarPage.goToIdentityProviders();
|
||||
|
||||
|
@ -159,6 +156,32 @@ describe("Identity provider test", () => {
|
|||
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", () => {
|
||||
const modalUtils = new ModalUtils();
|
||||
|
||||
|
@ -172,11 +195,6 @@ describe("Identity provider test", () => {
|
|||
modalUtils.checkModalTitle(deletePrompt).confirmModal();
|
||||
masthead.checkNotificationMessage(deleteSuccessMsg);
|
||||
|
||||
sidebarPage.goToIdentityProviders();
|
||||
listingPage.itemExist("github").deleteItem("github");
|
||||
modalUtils.checkModalTitle(deletePrompt).confirmModal();
|
||||
masthead.checkNotificationMessage(deleteSuccessMsg);
|
||||
|
||||
sidebarPage.goToIdentityProviders();
|
||||
listingPage.itemExist("oidc").deleteItem("oidc");
|
||||
modalUtils.checkModalTitle(deletePrompt).confirmModal();
|
||||
|
|
|
@ -113,4 +113,42 @@ export default class AddMapperPage {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,8 @@ export const AttributesForm = ({
|
|||
|
||||
const columns = ["Key", "Value"];
|
||||
|
||||
const noSaveCancelButtons = !save && !reset;
|
||||
|
||||
const watchLast = inConfig
|
||||
? watch(`config.attributes[${fields.length - 1}].key`, "")
|
||||
: watch(`attributes[${fields.length - 1}].key`, "");
|
||||
|
@ -78,8 +80,6 @@ export const AttributesForm = ({
|
|||
}
|
||||
}, [fields]);
|
||||
|
||||
const noSaveCancelButtons = !save && !reset;
|
||||
|
||||
return (
|
||||
<FormAccess
|
||||
role="manage-realm"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 _ from "lodash";
|
||||
import {
|
||||
|
@ -34,7 +34,8 @@ import { upperCaseFormatter } from "../util";
|
|||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { ProviderIconMapper } from "./ProviderIconMapper";
|
||||
import { ManageOderDialog } from "./ManageOrderDialog";
|
||||
import { toIdentityProviderTab } from "./routes/IdentityProviderTab";
|
||||
import { toIdentityProvider } from "./routes/IdentityProvider";
|
||||
import { toIdentityProviderCreate } from "./routes/IdentityProviderCreate";
|
||||
|
||||
export const IdentityProvidersSection = () => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
|
@ -43,7 +44,6 @@ export const IdentityProvidersSection = () => {
|
|||
"groupName"
|
||||
);
|
||||
const { realm } = useRealm();
|
||||
const { url } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
@ -73,9 +73,9 @@ export const IdentityProvidersSection = () => {
|
|||
const DetailLink = (identityProvider: IdentityProviderRepresentation) => (
|
||||
<Link
|
||||
key={identityProvider.providerId}
|
||||
to={toIdentityProviderTab({
|
||||
to={toIdentityProvider({
|
||||
realm,
|
||||
providerId: identityProvider.providerId,
|
||||
providerId: identityProvider.providerId!,
|
||||
alias: identityProvider.alias!,
|
||||
tab: "settings",
|
||||
})}
|
||||
|
@ -96,7 +96,12 @@ export const IdentityProvidersSection = () => {
|
|||
);
|
||||
|
||||
const navigateToCreate = (providerId: string) =>
|
||||
history.push(`${url}/${providerId}`);
|
||||
history.push(
|
||||
toIdentityProviderCreate({
|
||||
realm,
|
||||
providerId,
|
||||
})
|
||||
);
|
||||
|
||||
const identityProviderOptions = () =>
|
||||
Object.keys(identityProviders).map((group) => (
|
||||
|
@ -105,11 +110,18 @@ export const IdentityProvidersSection = () => {
|
|||
<DropdownItem
|
||||
key={provider.id}
|
||||
value={provider.id}
|
||||
component={
|
||||
<Link
|
||||
to={toIdentityProviderCreate({
|
||||
realm,
|
||||
providerId: provider.id,
|
||||
})}
|
||||
data-testid={provider.id}
|
||||
onClick={() => navigateToCreate(provider.id)}
|
||||
>
|
||||
{provider.name}
|
||||
</DropdownItem>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</DropdownGroup>
|
||||
));
|
||||
|
@ -243,7 +255,7 @@ export const IdentityProvidersSection = () => {
|
|||
},
|
||||
{
|
||||
name: "providerId",
|
||||
displayKey: "identity-providers:provider",
|
||||
displayKey: "identity-providers:providerDetails",
|
||||
cellFormatters: [upperCaseFormatter()],
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
PageSection,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type { BreadcrumbData } from "use-react-router-breadcrumbs";
|
||||
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
|
||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||
import { toUpperCase } from "../../util";
|
||||
|
@ -18,34 +17,12 @@ import { useAdminClient } from "../../context/auth/AdminClient";
|
|||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { GeneralSettings } from "./GeneralSettings";
|
||||
import { toIdentityProviderTab } from "../routes/IdentityProviderTab";
|
||||
|
||||
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),
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
import { toIdentityProvider } from "../routes/IdentityProvider";
|
||||
import type { IdentityProviderCreateParams } from "../routes/IdentityProviderCreate";
|
||||
|
||||
export const AddIdentityProvider = () => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { providerId } = useParams<IdentityProviderCreateParams>();
|
||||
const form = useForm<IdentityProviderRepresentation>();
|
||||
const {
|
||||
handleSubmit,
|
||||
|
@ -61,11 +38,18 @@ export const AddIdentityProvider = () => {
|
|||
try {
|
||||
await adminClient.identityProviders.create({
|
||||
...provider,
|
||||
providerId: id,
|
||||
alias: id,
|
||||
providerId,
|
||||
alias: providerId,
|
||||
});
|
||||
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) {
|
||||
addError("identity-providers:createError", error);
|
||||
}
|
||||
|
@ -74,7 +58,9 @@ export const AddIdentityProvider = () => {
|
|||
return (
|
||||
<>
|
||||
<ViewHeader
|
||||
titleKey={t("addIdentityProvider", { provider: toUpperCase(id) })}
|
||||
titleKey={t("addIdentityProvider", {
|
||||
provider: toUpperCase(providerId!),
|
||||
})}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
|
@ -83,7 +69,7 @@ export const AddIdentityProvider = () => {
|
|||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<GeneralSettings id={id} />
|
||||
<GeneralSettings id={providerId!} />
|
||||
</FormProvider>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
|
@ -32,6 +32,9 @@ import _ from "lodash";
|
|||
import { AssociatedRolesModal } from "../../realm-roles/AssociatedRolesModal";
|
||||
import type { RoleRepresentation } from "../../model/role-model";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import type { IdentityProviderEditMapperParams } from "../routes/EditMapper";
|
||||
import { convertToFormValues } from "../../util";
|
||||
import { toIdentityProvider } from "../routes/IdentityProvider";
|
||||
|
||||
type IdPMapperRepresentationWithAttributes =
|
||||
IdentityProviderMapperRepresentation & {
|
||||
|
@ -45,21 +48,47 @@ export const AddMapper = () => {
|
|||
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 { id } = useParams<IdentityProviderEditMapperParams>();
|
||||
|
||||
const isSAMLorOIDC = providerId === "saml" || providerId === "oidc";
|
||||
|
||||
const [mapperTypes, setMapperTypes] =
|
||||
useState<Record<string, IdentityProviderMapperRepresentation>>();
|
||||
const [currentMapper, setCurrentMapper] =
|
||||
useState<IdentityProviderMapperRepresentation>();
|
||||
const [roles, setRoles] = useState<RoleRepresentation[]>([]);
|
||||
|
||||
const [rolesModalOpen, setRolesModalOpen] = useState(false);
|
||||
|
||||
const save = async (idpMapper: IdentityProviderMapperRepresentation) => {
|
||||
if (id) {
|
||||
const updatedMapper = {
|
||||
...idpMapper,
|
||||
identityProviderAlias: alias!,
|
||||
id: id,
|
||||
name: currentMapper?.name!,
|
||||
config: {
|
||||
...idpMapper.config,
|
||||
attributes: JSON.stringify(idpMapper.config?.attributes!),
|
||||
},
|
||||
};
|
||||
try {
|
||||
await adminClient.identityProviders.updateMapper(
|
||||
{
|
||||
id: id!,
|
||||
alias: alias!,
|
||||
},
|
||||
updatedMapper
|
||||
);
|
||||
addAlert(t("mapperSaveSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError(t("mapperSaveError"), error);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await adminClient.identityProviders.createMapper({
|
||||
identityProviderMapper: {
|
||||
|
@ -76,30 +105,62 @@ export const AddMapper = () => {
|
|||
} catch (error) {
|
||||
addError(t("mapperCreateError"), error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
const { append, remove, fields } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "attributes",
|
||||
name: "config.attributes",
|
||||
});
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const allMapperTypes =
|
||||
await adminClient.identityProviders.findMapperTypes({
|
||||
alias: alias!,
|
||||
});
|
||||
() =>
|
||||
Promise.all([
|
||||
id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
|
||||
adminClient.identityProviders.findMapperTypes({ alias }),
|
||||
!id ? adminClient.roles.find() : null,
|
||||
]),
|
||||
([mapper, mapperTypes, roles]) => {
|
||||
if (mapper) {
|
||||
setCurrentMapper(mapper);
|
||||
setupForm(mapper);
|
||||
}
|
||||
|
||||
const allRoles = await adminClient.roles.find();
|
||||
return { allMapperTypes, allRoles };
|
||||
},
|
||||
({ allMapperTypes, allRoles }) => {
|
||||
setMapperTypes(allMapperTypes);
|
||||
setRoles(allRoles);
|
||||
setMapperTypes(mapperTypes);
|
||||
|
||||
if (roles) {
|
||||
setRoles(roles);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
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 [syncModeOpen, setSyncModeOpen] = useState(false);
|
||||
const [mapperTypeOpen, setMapperTypeOpen] = useState(false);
|
||||
|
@ -113,7 +174,7 @@ export const AddMapper = () => {
|
|||
<PageSection variant="light">
|
||||
<ViewHeader
|
||||
className="kc-add-mapper-title"
|
||||
titleKey={t("addIdPMapper")}
|
||||
titleKey={id ? t("editIdPMapper") : t("addIdPMapper")}
|
||||
divider
|
||||
/>
|
||||
<AssociatedRolesModal
|
||||
|
@ -122,6 +183,7 @@ export const AddMapper = () => {
|
|||
open={rolesModalOpen}
|
||||
omitComposites
|
||||
isRadio
|
||||
isMapperId
|
||||
toggleDialog={toggleModal}
|
||||
/>
|
||||
<FormAccess
|
||||
|
@ -130,6 +192,29 @@ export const AddMapper = () => {
|
|||
onSubmit={handleSubmit(save)}
|
||||
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
|
||||
label={t("common:name")}
|
||||
labelIcon={
|
||||
|
@ -153,6 +238,7 @@ export const AddMapper = () => {
|
|||
datatest-id="name-input"
|
||||
id="kc-name"
|
||||
name="name"
|
||||
isDisabled={!!id}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
|
@ -217,12 +303,17 @@ export const AddMapper = () => {
|
|||
>
|
||||
<Controller
|
||||
name="identityProviderMapper"
|
||||
defaultValue={"saml-advanced-role-idp-mapper"}
|
||||
defaultValue={
|
||||
providerId === "saml"
|
||||
? "saml-advanced-role-idp-mapper"
|
||||
: "oidc-advanced-role-idp-mapper"
|
||||
}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="identityProviderMapper"
|
||||
data-testid="idp-mapper-select"
|
||||
isDisabled={!!id}
|
||||
required
|
||||
direction="down"
|
||||
onToggle={() => setMapperTypeOpen(!mapperTypeOpen)}
|
||||
|
@ -342,7 +433,7 @@ export const AddMapper = () => {
|
|||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-role"
|
||||
data-testid="mapper-role-input"
|
||||
|
@ -440,7 +531,17 @@ export const AddMapper = () => {
|
|||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => history.push(`/${realm}/client-scopes`)}
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={toIdentityProvider({
|
||||
realm,
|
||||
providerId,
|
||||
alias: alias!,
|
||||
tab: "settings",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React 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";
|
||||
|
@ -11,6 +11,7 @@ import {
|
|||
DropdownItem,
|
||||
Form,
|
||||
PageSection,
|
||||
Spinner,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
ToolbarItem,
|
||||
|
@ -38,7 +39,10 @@ import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTa
|
|||
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 { toIdentityProviderEditMapper } from "../routes/EditMapper";
|
||||
|
||||
import { toUpperCase } from "../../util";
|
||||
import type { IdentityProviderParams } from "../routes/IdentityProvider";
|
||||
|
||||
type HeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
|
@ -52,6 +56,7 @@ type IdPWithMapperAttributes = IdentityProviderMapperRepresentation & {
|
|||
category?: string;
|
||||
helpText?: string;
|
||||
type: string;
|
||||
mapperId: string;
|
||||
};
|
||||
|
||||
const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
|
||||
|
@ -95,21 +100,35 @@ const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
|
|||
|
||||
export const DetailSettings = () => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
const { providerId, alias } =
|
||||
useParams<{ providerId: string; alias: string }>();
|
||||
const { alias, providerId } = useParams<IdentityProviderParams>();
|
||||
|
||||
const form = useForm<IdentityProviderRepresentation>();
|
||||
const { handleSubmit, getValues, reset } = form;
|
||||
const [provider, setProvider] = useState<IdentityProviderRepresentation>();
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const history = useHistory();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const MapperLink = ({ name, mapperId }: IdPWithMapperAttributes) => (
|
||||
<Link
|
||||
to={toIdentityProviderEditMapper({
|
||||
realm,
|
||||
alias,
|
||||
providerId: provider?.providerId!,
|
||||
id: mapperId,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
|
||||
useFetch(
|
||||
() => adminClient.identityProviders.findOne({ alias: alias }),
|
||||
() => adminClient.identityProviders.findOne({ alias }),
|
||||
(fetchedProvider) => {
|
||||
reset(fetchedProvider);
|
||||
setProvider(fetchedProvider);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
@ -143,10 +162,14 @@ export const DetailSettings = () => {
|
|||
},
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const sections = [t("generalSettings"), t("advancedSettings")];
|
||||
|
||||
const isOIDC = providerId.includes("oidc");
|
||||
const isSAML = providerId.includes("saml");
|
||||
const isOIDC = provider.providerId!.includes("oidc");
|
||||
const isSAML = provider.providerId!.includes("saml");
|
||||
|
||||
const loader = async () => {
|
||||
const [loaderMappers, loaderMapperTypes] = await Promise.all([
|
||||
|
@ -164,6 +187,7 @@ export const DetailSettings = () => {
|
|||
...mapperType,
|
||||
name: loaderMapper.name!,
|
||||
type: mapperType?.name!,
|
||||
mapperId: loaderMapper.id!,
|
||||
};
|
||||
|
||||
return result;
|
||||
|
@ -243,7 +267,7 @@ export const DetailSettings = () => {
|
|||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<AdvancedSettings isOIDC={isOIDC} isSAML={isSAML} />
|
||||
<AdvancedSettings isOIDC={isOIDC!} isSAML={isSAML!} />
|
||||
|
||||
<ActionGroup className="keycloak__form_actions">
|
||||
<Button data-testid={"save"} type="submit">
|
||||
|
@ -279,7 +303,8 @@ export const DetailSettings = () => {
|
|||
toIdentityProviderAddMapper({
|
||||
realm,
|
||||
alias: alias!,
|
||||
providerId: providerId,
|
||||
providerId: provider.providerId!,
|
||||
tab: "mappers",
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -296,7 +321,8 @@ export const DetailSettings = () => {
|
|||
to={toIdentityProviderAddMapper({
|
||||
realm,
|
||||
alias: alias!,
|
||||
providerId: providerId,
|
||||
providerId: provider.providerId!,
|
||||
tab: "mappers",
|
||||
})}
|
||||
datatest-id="add-mapper-button"
|
||||
>
|
||||
|
@ -308,6 +334,7 @@ export const DetailSettings = () => {
|
|||
{
|
||||
name: "name",
|
||||
displayKey: "common:name",
|
||||
cellRenderer: MapperLink,
|
||||
},
|
||||
{
|
||||
name: "category",
|
||||
|
|
|
@ -7,13 +7,13 @@ import { HelpItem } from "../../components/help-enabler/HelpItem";
|
|||
import { RedirectUrl } from "../component/RedirectUrl";
|
||||
import { TextField } from "../component/TextField";
|
||||
import { DisplayOrder } from "../component/DisplayOrder";
|
||||
import type { IdentityProviderTabParams } from "../routes/IdentityProviderTab";
|
||||
import type { IdentityProviderParams } from "../routes/IdentityProvider";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export const OIDCGeneralSettings = ({ id }: { id: string }) => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
const { t: th } = useTranslation("identity-providers-help");
|
||||
const { tab } = useParams<IdentityProviderTabParams>();
|
||||
const { tab } = useParams<IdentityProviderParams>();
|
||||
|
||||
const { register, errors } = useFormContext();
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@ import { RedirectUrl } from "../component/RedirectUrl";
|
|||
import { TextField } from "../component/TextField";
|
||||
import { DisplayOrder } from "../component/DisplayOrder";
|
||||
import { useParams } from "react-router";
|
||||
import type { IdentityProviderTabParams } from "../routes/IdentityProviderTab";
|
||||
import type { IdentityProviderParams } from "../routes/IdentityProvider";
|
||||
|
||||
export const SamlGeneralSettings = ({ id }: { id: string }) => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
const { t: th } = useTranslation("identity-providers-help");
|
||||
const { tab } = useParams<IdentityProviderTabParams>();
|
||||
const { tab } = useParams<IdentityProviderParams>();
|
||||
|
||||
const { register, errors } = useFormContext();
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ export default {
|
|||
listExplain:
|
||||
"Through Identity Brokering it's easy to allow users to authenticate to Keycloak using external Identity Provider or Social Networks.",
|
||||
searchForProvider: "Search for provider",
|
||||
provider: "Provider details",
|
||||
providerDetails: "Provider details",
|
||||
addProvider: "Add provider",
|
||||
addMapper: "Add mapper",
|
||||
addIdPMapper: "Add Identity Provider Mapper",
|
||||
editIdPMapper: "Edit Identity Provider Mapper",
|
||||
mappersList: "Mappers list",
|
||||
noMappers: "No Mappers",
|
||||
noMappersInstructions:
|
||||
|
@ -160,6 +161,8 @@ export default {
|
|||
selectRole: "Select role",
|
||||
mapperCreateSuccess: "Mapper created successfully.",
|
||||
mapperCreateError: "Error creating mapper.",
|
||||
mapperSaveSuccess: "Mapper saved successfully.",
|
||||
mapperSaveError: "Error saving mapper: {{error}}",
|
||||
userSessionAttribute: "User Session Attribute",
|
||||
userSessionAttributeValue: "User Session Attribute Value",
|
||||
},
|
||||
|
|
|
@ -4,17 +4,19 @@ import { IdentityProviderKeycloakOidcRoute } from "./routes/IdentityProviderKeyc
|
|||
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";
|
||||
import { IdentityProviderEditMapperRoute } from "./routes/EditMapper";
|
||||
import { IdentityProviderCreateRoute } from "./routes/IdentityProviderCreate";
|
||||
|
||||
const routes: RouteDef[] = [
|
||||
IdentityProviderAddMapperRoute,
|
||||
IdentityProviderEditMapperRoute,
|
||||
IdentityProvidersRoute,
|
||||
IdentityProviderOidcRoute,
|
||||
IdentityProviderSamlRoute,
|
||||
IdentityProviderKeycloakOidcRoute,
|
||||
IdentityProviderCreateRoute,
|
||||
IdentityProviderRoute,
|
||||
IdentityProviderTabRoute,
|
||||
IdentityProviderAddMapperRoute,
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
|
|
@ -7,12 +7,14 @@ export type IdentityProviderAddMapperParams = {
|
|||
realm: string;
|
||||
providerId: string;
|
||||
alias: string;
|
||||
tab: string;
|
||||
};
|
||||
|
||||
export const IdentityProviderAddMapperRoute: RouteDef = {
|
||||
path: "/:realm/identity-providers/:providerId/:alias/mappers/create",
|
||||
path: "/:realm/identity-providers/:providerId/:alias/:tab/create",
|
||||
component: AddMapper,
|
||||
access: "manage-identity-providers",
|
||||
breadcrumb: (t) => t("identity-providers:addIdPMapper"),
|
||||
};
|
||||
|
||||
export const toIdentityProviderAddMapper = (
|
||||
|
|
24
src/identity-providers/routes/EditMapper.ts
Normal file
24
src/identity-providers/routes/EditMapper.ts
Normal 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),
|
||||
});
|
|
@ -1,20 +1,21 @@
|
|||
import type { LocationDescriptorObject } from "history";
|
||||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
import {
|
||||
AddIdentityProvider,
|
||||
IdentityProviderCrumb,
|
||||
} from "../add/AddIdentityProvider";
|
||||
import { DetailSettings } from "../add/DetailSettings";
|
||||
|
||||
type IdentityProviderTabs = "settings" | "mappers";
|
||||
|
||||
export type IdentityProviderParams = {
|
||||
realm: string;
|
||||
id: string;
|
||||
providerId: string;
|
||||
alias: string;
|
||||
tab: IdentityProviderTabs;
|
||||
};
|
||||
|
||||
export const IdentityProviderRoute: RouteDef = {
|
||||
path: "/:realm/identity-providers/:id",
|
||||
component: AddIdentityProvider,
|
||||
breadcrumb: () => IdentityProviderCrumb,
|
||||
path: "/:realm/identity-providers/:providerId/:alias/:tab",
|
||||
component: DetailSettings,
|
||||
breadcrumb: (t) => t("identity-providers:providerDetails"),
|
||||
access: "manage-identity-providers",
|
||||
};
|
||||
|
||||
|
|
22
src/identity-providers/routes/IdentityProviderCreate.ts
Normal file
22
src/identity-providers/routes/IdentityProviderCreate.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 { 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),
|
||||
});
|
|
@ -6,7 +6,7 @@ import { AddOpenIdConnect } from "../add/AddOpenIdConnect";
|
|||
export type IdentityProviderKeycloakOidcParams = { realm: string };
|
||||
|
||||
export const IdentityProviderKeycloakOidcRoute: RouteDef = {
|
||||
path: "/:realm/identity-providers/keycloak-oidc",
|
||||
path: "/:realm/identity-providers/keycloak-oidc/add",
|
||||
component: AddOpenIdConnect,
|
||||
breadcrumb: (t) => t("identity-providers:addKeycloakOpenIdProvider"),
|
||||
access: "manage-identity-providers",
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AddOpenIdConnect } from "../add/AddOpenIdConnect";
|
|||
export type IdentityProviderOidcParams = { realm: string };
|
||||
|
||||
export const IdentityProviderOidcRoute: RouteDef = {
|
||||
path: "/:realm/identity-providers/oidc",
|
||||
path: "/:realm/identity-providers/oidc/add",
|
||||
component: AddOpenIdConnect,
|
||||
breadcrumb: (t) => t("identity-providers:addOpenIdProvider"),
|
||||
access: "manage-identity-providers",
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AddSamlConnect } from "../add/AddSamlConnect";
|
|||
export type IdentityProviderSamlParams = { realm: string };
|
||||
|
||||
export const IdentityProviderSamlRoute: RouteDef = {
|
||||
path: "/:realm/identity-providers/saml",
|
||||
path: "/:realm/identity-providers/saml/add",
|
||||
component: AddSamlConnect,
|
||||
breadcrumb: (t) => t("identity-providers:addSamlProvider"),
|
||||
access: "manage-identity-providers",
|
||||
|
|
|
@ -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),
|
||||
});
|
|
@ -16,6 +16,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
|
|||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
|
||||
import _ from "lodash";
|
||||
import type { RealmRoleParams } from "./routes/RealmRole";
|
||||
|
||||
type Role = RoleRepresentation & {
|
||||
clientId?: string;
|
||||
|
@ -29,6 +30,7 @@ export type AssociatedRolesModalProps = {
|
|||
allRoles?: RoleRepresentation[];
|
||||
omitComposites?: boolean;
|
||||
isRadio?: boolean;
|
||||
isMapperId?: boolean;
|
||||
};
|
||||
|
||||
export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||
|
@ -42,7 +44,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { id } = useParams<RealmRoleParams>();
|
||||
|
||||
const alphabetize = (rolesList: RoleRepresentation[]) => {
|
||||
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
|
||||
|
@ -144,16 +146,11 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
}, [filterType]);
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
if (id) return await adminClient.roles.findOneById({ id });
|
||||
},
|
||||
(fetchedRole) => {
|
||||
if (fetchedRole) {
|
||||
setName(fetchedRole.name!);
|
||||
} else {
|
||||
setName(t("createRole"));
|
||||
}
|
||||
},
|
||||
() =>
|
||||
!props.isMapperId
|
||||
? adminClient.roles.findOneById({ id })
|
||||
: Promise.resolve(null),
|
||||
(role) => setName(role ? role.name! : t("createRole")),
|
||||
[]
|
||||
);
|
||||
|
||||
|
@ -183,7 +180,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
key="add"
|
||||
data-testid="add-associated-roles-button"
|
||||
variant="primary"
|
||||
isDisabled={!selectedRows?.length}
|
||||
isDisabled={!selectedRows.length}
|
||||
onClick={() => {
|
||||
props.toggleDialog();
|
||||
props.onConfirm(selectedRows);
|
||||
|
|
|
@ -23,7 +23,7 @@ import _ from "lodash";
|
|||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { UserIdpModal } from "./UserIdPModal";
|
||||
import { toIdentityProviderTab } from "../identity-providers/routes/IdentityProviderTab";
|
||||
import { toIdentityProvider } from "../identity-providers/routes/IdentityProvider";
|
||||
|
||||
export const UserIdentityProviderLinks = () => {
|
||||
const [key, setKey] = useState(0);
|
||||
|
@ -42,10 +42,25 @@ export const UserIdentityProviderLinks = () => {
|
|||
setIsLinkIdPModalOpen(!isLinkIdPModalOpen);
|
||||
};
|
||||
|
||||
type withProviderId = FederatedIdentityRepresentation & {
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
const identityProviders = useServerInfo().identityProviders;
|
||||
|
||||
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 () => {
|
||||
|
@ -89,11 +104,12 @@ export const UserIdentityProviderLinks = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const idpLinkRenderer = (idp: FederatedIdentityRepresentation) => {
|
||||
const idpLinkRenderer = (idp: withProviderId) => {
|
||||
return (
|
||||
<Link
|
||||
to={toIdentityProviderTab({
|
||||
to={toIdentityProvider({
|
||||
realm,
|
||||
providerId: idp.providerId,
|
||||
alias: idp.identityProvider!,
|
||||
tab: "settings",
|
||||
})}
|
||||
|
|
Loading…
Reference in a new issue