From 95c22fd86224f459df30fe3636ffdc134db740c4 Mon Sep 17 00:00:00 2001 From: mfrances Date: Mon, 22 Mar 2021 17:04:40 -0400 Subject: [PATCH] preliminary work - list working --- .../UserFederationLdapSettings.tsx | 5 +- .../ldap/mappers/LdapMapperDialog.tsx | 201 ++++++++++ .../ldap/mappers/LdapMapperList.tsx | 142 +++++++ .../ldap/mappers/LdapMappingDetails.tsx | 376 ++++++++++++++++++ .../ldap/mappers/LdapRoleMappingForm.tsx | 314 +++++++++++++++ 5 files changed, 1035 insertions(+), 3 deletions(-) create mode 100644 src/user-federation/ldap/mappers/LdapMapperDialog.tsx create mode 100644 src/user-federation/ldap/mappers/LdapMapperList.tsx create mode 100644 src/user-federation/ldap/mappers/LdapMappingDetails.tsx create mode 100644 src/user-federation/ldap/mappers/LdapRoleMappingForm.tsx diff --git a/src/user-federation/UserFederationLdapSettings.tsx b/src/user-federation/UserFederationLdapSettings.tsx index d6b78b7c7a..686a13a668 100644 --- a/src/user-federation/UserFederationLdapSettings.tsx +++ b/src/user-federation/UserFederationLdapSettings.tsx @@ -10,7 +10,6 @@ import { PageSection, Tab, TabTitleText, - Text, } from "@patternfly/react-core"; import { LdapSettingsAdvanced } from "./ldap/LdapSettingsAdvanced"; @@ -35,6 +34,7 @@ import { useHistory, useParams } from "react-router-dom"; import { ScrollForm } from "../components/scroll-form/ScrollForm"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; +import {LdapMapperList} from "./ldap/mappers/LdapMapperList"; type LdapSettingsHeaderProps = { onChange: (value: string) => void; @@ -331,8 +331,7 @@ export const UserFederationLdapSettings = () => { eventKey="mappers" title={{t("common:mappers")}} > - {/* */} - Coming soon! + diff --git a/src/user-federation/ldap/mappers/LdapMapperDialog.tsx b/src/user-federation/ldap/mappers/LdapMapperDialog.tsx new file mode 100644 index 0000000000..e94b741e00 --- /dev/null +++ b/src/user-federation/ldap/mappers/LdapMapperDialog.tsx @@ -0,0 +1,201 @@ +import React, { useState } from "react"; +import { + Button, + ButtonVariant, + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + Modal, + ModalVariant, + Text, + TextContent, +} from "@patternfly/react-core"; +import { + Table, + TableBody, + TableHeader, + TableVariant, +} from "@patternfly/react-table"; +import { useTranslation } from "react-i18next"; +import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation"; +import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation"; + +import { useServerInfo } from "../../../context/server-info/ServerInfoProvider"; +import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState"; + +// export type AddLdapMapperDialogModalProps = { +// protocol: string; +// filter?: ProtocolMapperRepresentation[]; +// onConfirm: ( +// value: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[] +// ) => void; +// }; + +export type AddLdapMapperDialogProps = { + open: boolean; + toggleDialog: () => void; +}; + +export const AddLdapMapperDialog = (props: AddLdapMapperDialogProps) => { + const { t } = useTranslation("client-scopes"); + + const serverInfo = useServerInfo(); + // const protocol = props.protocol; + // const protocolMappers = serverInfo.protocolMapperTypes![protocol]; + // const builtInMappers = serverInfo.builtinProtocolMappers![protocol]; + // const [filter, setFilter] = useState([]); + + // const allRows = builtInMappers.map((mapper) => { + // const mapperType = protocolMappers.filter( + // (type) => type.id === mapper.protocolMapper + // )[0]; + // return { + // item: mapper, + // selected: false, + // cells: [mapper.name, mapperType.helpText], + // }; + // }); + const [rows, setRows] = useState(allRows); + + // if (props.filter && props.filter.length !== filter.length) { + // setFilter(props.filter); + // const nameFilter = props.filter.map((f) => f.name); + // setRows([...allRows.filter((row) => !nameFilter.includes(row.item.name))]); + // } + + // const selectedRows = rows + // .filter((row) => row.selected) + // .map((row) => row.item); + + //const isBuiltIn = !!props.filter; + const isBuiltIn = true; + + const mapperList = [ + { + "id":"699b72e5-b936-41b9-98fc-5d5b3ec5ea6f", + "name":"username", + "providerId":"user-attribute-ldap-mapper", + "providerType":"org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + "parentId":"f1da61f9-08f7-4dc5-83dd-d774fba518d4", + "config": + { + "ldap.attribute":["cn"], + "is.mandatory.in.ldap":["true"], + "always.read.value.from.ldap":["false"], + "read.only":["true"], + "user.model.attribute":["username"] + } + }, + { + "id":"c11788d2-62be-442f-813f-4b00382a1e10", + "name":"last name", + "providerId":"user-attribute-ldap-mapper", + "providerType":"org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + "parentId":"f1da61f9-08f7-4dc5-83dd-d774fba518d4", + "config": + { + "ldap.attribute":["sn"], + "is.mandatory.in.ldap":["true"], + "always.read.value.from.ldap":["true"], + "read.only":["true"], + "user.model.attribute":["lastName"] + } + } + ] + + return ( + { + // props.onConfirm(selectedRows); + props.toggleDialog(); + }} + > + {t("common:add")} + , + , + ] + : [] + } + > + + {t("predefinedMappingDescription")} + + {!isBuiltIn && ( + { + const mapper = mapperList.find((mapper) => mapper.id === id); + // props.onConfirm(mapper!); + props.toggleDialog(); + }} + aria-label={t("chooseAMapperType")} + isCompact + > + {( + + + + <>{mapperList[0].name} + , + + <>{mapperList[0].helpText} + , + ]} + /> + + + ))} + + )} + {isBuiltIn && rows.length > 0 && ( + { + rows[rowIndex].selected = isSelected; + setRows([...rows]); + }} + canSelectAll={false} + rows={rows} + aria-label={t("chooseAMapperType")} + > + + +
+ )} + {isBuiltIn && rows.length === 0 && ( + + )} +
+ ); +}; diff --git a/src/user-federation/ldap/mappers/LdapMapperList.tsx b/src/user-federation/ldap/mappers/LdapMapperList.tsx new file mode 100644 index 0000000000..5caf577318 --- /dev/null +++ b/src/user-federation/ldap/mappers/LdapMapperList.tsx @@ -0,0 +1,142 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { AlertVariant } from "@patternfly/react-core"; +import { + Table, + TableBody, + TableHeader, + TableVariant, +} from "@patternfly/react-table"; +import { useErrorHandler } from "react-error-boundary"; + +import { TableToolbar } from "../../../components/table-toolbar/TableToolbar"; +import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState"; +import { useAlerts } from "../../../components/alert/Alerts"; +import { + useAdminClient, + asyncStateFetch, +} from "../../../context/auth/AdminClient"; + +import { useParams } from "react-router-dom"; + +interface ComponentMapperRepresentation { + config?: Record; + id?: string; + name?: string; + providerId?: string; + providerType?: string; + parentID?: string; +} + +type Row = { + name: JSX.Element; + type: string; +}; + +export const LdapMapperList = () => { + const [mappers, setMappers] = useState(); + + const { t } = useTranslation("client-scopes"); + const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + const handleError = useErrorHandler(); + const [key, setKey] = useState(0); + + const { id } = useParams<{ id: string }>(); + + useEffect(() => { + return asyncStateFetch( + () => { + const testParams: { [name: string]: string | number } = { + parent: id, + type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + }; + return adminClient.components.find(testParams); + }, + (mappers) => { + setMappers(mappers); + console.log(mappers); + }, + handleError + ); + }, [key]); + + if (!mappers) { + return ( + <> + + + ); + } + + return ( + + { + return { + cells: Object.values([cell.name, cell.providerId]), + }; + })} + aria-label={t("clientScopeList")} + actions={[ + { + title: t("common:delete"), + onClick: () => { + addAlert(t("mappingDeletedSuccess"), AlertVariant.success); + }, + }, + ]} + > + + +
+
+ ); +}; + +/* +Sample responses: + +const mapperList = [ + { + id: "699b72e5-b936-41b9-98fc-5d5b3ec5ea6f", + name: "username", + providerId: "user-attribute-ldap-mapper", + providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + parentId: "f1da61f9-08f7-4dc5-83dd-d774fba518d4", + config: { + "ldap.attribute": ["cn"], + "is.mandatory.in.ldap": ["true"], + "always.read.value.from.ldap": ["false"], + "read.only": ["true"], + "user.model.attribute": ["username"], + }, + }, + { + id: "c11788d2-62be-442f-813f-4b00382a1e10", + name: "last name", + providerId: "user-attribute-ldap-mapper", + providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + parentId: "f1da61f9-08f7-4dc5-83dd-d774fba518d4", + config: { + "ldap.attribute": ["sn"], + "is.mandatory.in.ldap": ["true"], + "always.read.value.from.ldap": ["true"], + "read.only": ["true"], + "user.model.attribute": ["lastName"], + }, + }, +]; +*/ diff --git a/src/user-federation/ldap/mappers/LdapMappingDetails.tsx b/src/user-federation/ldap/mappers/LdapMappingDetails.tsx new file mode 100644 index 0000000000..0beb18ed86 --- /dev/null +++ b/src/user-federation/ldap/mappers/LdapMappingDetails.tsx @@ -0,0 +1,376 @@ +import React, { useEffect, useState } from "react"; +import { useHistory, useParams, useRouteMatch } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useErrorHandler } from "react-error-boundary"; +import { + ActionGroup, + AlertVariant, + Button, + ButtonVariant, + Checkbox, + DropdownItem, + Flex, + FlexItem, + FormGroup, + PageSection, + Select, + SelectOption, + SelectVariant, + Switch, + TextInput, + ValidatedOptions, +} from "@patternfly/react-core"; +import { ConfigPropertyRepresentation } from "keycloak-admin/lib/defs/configPropertyRepresentation"; +import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation"; + +import { ViewHeader } from "../../../components/view-header/ViewHeader"; +import { + useAdminClient, + asyncStateFetch, +} from "../../../context/auth/AdminClient"; +import { Controller, useForm } from "react-hook-form"; +import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog"; +import { useAlerts } from "../../../components/alert/Alerts"; +import { HelpItem } from "../../../components/help-enabler/HelpItem"; +import { useServerInfo } from "../../../context/server-info/ServerInfoProvider"; +import { convertFormValuesToObject, convertToFormValues } from "../../../util"; +import { FormAccess } from "../../../components/form-access/FormAccess"; + +type Params = { + id: string; + mapperId: string; +}; + +export const LdapMappingDetails = () => { + const { t } = useTranslation("client-scopes"); + const adminClient = useAdminClient(); + const handleError = useErrorHandler(); + const { addAlert } = useAlerts(); + + const { id, mapperId } = useParams(); + const { register, errors, setValue, control, handleSubmit } = useForm(); + const [mapping, setMapping] = useState(); + const [typeOpen, setTypeOpen] = useState(false); + const [configProperties, setConfigProperties] = useState< + ConfigPropertyRepresentation[] + >(); + + const history = useHistory(); + const serverInfo = useServerInfo(); + const { url } = useRouteMatch(); + const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/; + + useEffect(() => { + return asyncStateFetch( + async () => { + if (mapperId.match(isGuid)) { + const data = await adminClient.clientScopes.findProtocolMapper({ + id, + mapperId, + }); + if (data) { + Object.entries(data).map((entry) => { + convertToFormValues(entry[1], "config", setValue); + }); + } + const mapperTypes = serverInfo.protocolMapperTypes![data!.protocol!]; + const properties = mapperTypes.find( + (type) => type.id === data!.protocolMapper + )?.properties!; + + return { + configProperties: properties, + mapping: data, + }; + } else { + const scope = await adminClient.clientScopes.findOne({ id }); + const protocolMappers = serverInfo.protocolMapperTypes![ + scope.protocol! + ]; + const mapping = protocolMappers.find( + (mapper) => mapper.id === mapperId + )!; + return { + mapping: { + name: mapping.name, + protocol: scope.protocol, + protocolMapper: mapperId, + }, + }; + } + }, + (result) => { + setConfigProperties(result.configProperties); + setMapping(result.mapping); + }, + handleError + ); + }, []); + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "client-scopes:deleteMappingTitle", + messageKey: "client-scopes:deleteMappingConfirm", + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.clientScopes.delClientScopeMappings( + { client: id, id: mapperId }, + [] + ); + addAlert(t("mappingDeletedSuccess"), AlertVariant.success); + history.push(`${url}/${id}`); + } catch (error) { + addAlert(t("mappingDeletedError", { error }), AlertVariant.danger); + } + }, + }); + + const save = async (formMapping: ProtocolMapperRepresentation) => { + const config = convertFormValuesToObject(formMapping.config); + const map = { ...mapping, ...formMapping, config }; + const key = mapperId.match(isGuid) ? "Updated" : "Created"; + try { + if (mapperId.match(isGuid)) { + await adminClient.clientScopes.updateProtocolMapper( + { id, mapperId }, + map + ); + } else { + await adminClient.clientScopes.addProtocolMapper({ id }, map); + } + addAlert(t(`mapping${key}Success`), AlertVariant.success); + } catch (error) { + addAlert(t(`mapping${key}Error`, { error }), AlertVariant.danger); + } + }; + + return ( + <> + + + {t("common:delete")} + , + ] + : undefined + } + /> + + + <> + {!mapperId.match(isGuid) && ( + + } + fieldId="name" + isRequired + validated={ + errors.name + ? ValidatedOptions.error + : ValidatedOptions.default + } + helperTextInvalid={t("common:required")} + > + + + )} + + + } + fieldId="prefix" + > + + + + } + fieldId="multiValued" + > + ( + onChange("" + value)} + /> + )} + /> + + + } + fieldId="claimName" + > + + + + } + fieldId="claimJsonType" + > + ( + + )} + /> + + + + + ( + onChange("" + value)} + /> + )} + /> + + + ( + onChange("" + value)} + /> + )} + /> + + + ( + onChange("" + value)} + /> + )} + /> + + + + + + + + + + + ); +}; diff --git a/src/user-federation/ldap/mappers/LdapRoleMappingForm.tsx b/src/user-federation/ldap/mappers/LdapRoleMappingForm.tsx new file mode 100644 index 0000000000..1e8b2e5e02 --- /dev/null +++ b/src/user-federation/ldap/mappers/LdapRoleMappingForm.tsx @@ -0,0 +1,314 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useHistory, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Controller, useForm } from "react-hook-form"; +import { useErrorHandler } from "react-error-boundary"; +import { + FormGroup, + PageSection, + Select, + SelectVariant, + TextInput, + SelectOption, + ActionGroup, + Button, + SelectGroup, + Split, + SplitItem, + Divider, + ValidatedOptions, +} from "@patternfly/react-core"; + +import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; +import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; +import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation"; +import { useAlerts } from "../../../components/alert/Alerts"; +import { RealmContext } from "../../../context/realm-context/RealmContext"; +import { + useAdminClient, + asyncStateFetch, +} from "../../../context/auth/AdminClient"; + +import { ViewHeader } from "../../../components/view-header/ViewHeader"; +import { HelpItem } from "../../../components/help-enabler/HelpItem"; +import { FormAccess } from "../../../components/form-access/FormAccess"; + +export const RoleMappingForm = () => { + const { realm } = useContext(RealmContext); + const adminClient = useAdminClient(); + const handleError = useErrorHandler(); + const history = useHistory(); + const { addAlert } = useAlerts(); + + const { t } = useTranslation("client-scopes"); + const { register, handleSubmit, control, errors } = useForm(); + const { id } = useParams<{ id: string }>(); + + const [roleOpen, setRoleOpen] = useState(false); + + const [clientsOpen, setClientsOpen] = useState(false); + const [clients, setClients] = useState([]); + const [selectedClient, setSelectedClient] = useState(); + const [clientRoles, setClientRoles] = useState([]); + + useEffect(() => { + return asyncStateFetch( + async () => { + const clients = await adminClient.clients.find(); + + const asyncFilter = async ( + predicate: (client: ClientRepresentation) => Promise + ) => { + const results = await Promise.all(clients.map(predicate)); + return clients.filter((_, index) => results[index]); + }; + + const filteredClients = await asyncFilter( + async (client) => + (await adminClient.clients.listRoles({ id: client.id! })).length > 0 + ); + + filteredClients.map( + (client) => + (client.toString = function () { + return this.clientId!; + }) + ); + return filteredClients; + }, + (filteredClients) => setClients(filteredClients), + handleError + ); + }, []); + + useEffect(() => { + return asyncStateFetch( + async () => { + const client = selectedClient as ClientRepresentation; + if (client && client.name !== "realmRoles") { + const clientRoles = await adminClient.clients.listRoles({ + id: client.id!, + }); + return clientRoles; + } else { + return await adminClient.roles.find(); + } + }, + (clientRoles) => setClientRoles(clientRoles), + handleError + ); + }, [selectedClient]); + + const save = async (mapping: ProtocolMapperRepresentation) => { + try { + await adminClient.clientScopes.addProtocolMapper({ id }, mapping); + addAlert(t("mapperCreateSuccess")); + } catch (error) { + addAlert(t("mapperCreateError", error)); + } + }; + + const createSelectGroup = (clients: ClientRepresentation[]) => { + return [ + + t("realmRoles"), + } as ClientRepresentation + } + > + {realm} + + , + , + + {clients.map((client) => ( + + {client.clientId} + + ))} + , + ]; + }; + + const roleSelectOptions = () => { + const createItem = (role: RoleRepresentation) => ( + + {role.name} + + ); + return clientRoles.map((role) => createItem(role)); + }; + + return ( + <> + + + + + } + fieldId="protocolMapper" + > + + + + } + fieldId="name" + isRequired + validated={ + errors.name ? ValidatedOptions.error : ValidatedOptions.default + } + helperTextInvalid={t("common:required")} + > + + + + } + validated={errors["config.role"] ? "error" : "default"} + helperTextInvalid={t("common:required")} + fieldId="role" + > + + + + + + ( + + )} + /> + + + + + } + fieldId="newRoleName" + > + + + + + + + + + + ); +};