diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts index 5b06e330cb..d89fe90e5e 100644 --- a/cypress/integration/realm_settings_test.spec.ts +++ b/cypress/integration/realm_settings_test.spec.ts @@ -568,7 +568,7 @@ describe("Realm settings tests", () => { }); }); - describe.skip("Realm settings client policies tab tests", () => { + describe("Realm settings client policies tab tests", () => { beforeEach(() => { keycloakBefore(); loginPage.logIn(); diff --git a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts index b64f0faeb3..bd7f3137b1 100644 --- a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts +++ b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts @@ -176,8 +176,10 @@ export default class RealmSettingsPage { private clientPolicyDrpDwn = "action-dropdown"; private searchFld = "[id^=realm-settings][id$=profilesinput]"; private searchFldPolicies = "[id^=realm-settings][id$=clientPoliciesinput]"; - private clientProfileOne = 'a[href*="realm-settings/clientPolicies/Test"]'; - private clientProfileTwo = 'a[href*="realm-settings/clientPolicies/Edit"]'; + private clientProfileOne = + 'a[href*="realm-settings/clientPolicies/Test/edit-profile"]'; + private clientProfileTwo = + 'a[href*="realm-settings/clientPolicies/Edit/edit-profile"]'; private clientPolicy = 'a[href*="realm-settings/clientPolicies/Test/edit-policy"]'; private reloadBtn = "reloadProfile"; @@ -195,7 +197,6 @@ export default class RealmSettingsPage { private addConditionCancelBtn = "addCondition-cancelBtn"; private addConditionSaveBtn = "addCondition-saveBtn"; private conditionTypeLink = "condition-type-link"; - private addValue = "addValue"; private eventListenersFormLabel = ".pf-c-form__label-text"; private eventListenersDrpDwn = ".pf-c-select.kc_eventListeners_select"; private eventListenersSaveBtn = "saveEventListenerBtn"; @@ -206,6 +207,7 @@ export default class RealmSettingsPage { private eventListenersDrwDwnSelect = ".pf-c-button.pf-c-select__toggle-button.pf-m-plain"; private eventListenerRemove = '[data-ouia-component-id="Remove"]'; + private roleSelect = ".pf-c-select.kc-role-select"; selectLoginThemeType(themeType: string) { cy.get(this.selectLoginTheme).click(); @@ -971,7 +973,13 @@ export default class RealmSettingsPage { cy.findByTestId(this.addConditionDrpDwnOption) .contains("client-roles") .click(); - cy.get('input[name="config.roles[0].value"]').click().type("role 1"); + cy.get(this.roleSelect).click().contains("impersonation").click(); + + cy.get(this.roleSelect).contains("manage-realm").click(); + + cy.get(this.roleSelect).contains("view-users").click(); + + cy.get(this.roleSelect).click(); cy.findByTestId(this.addConditionSaveBtn).click(); cy.get(this.alertMessage).should( @@ -986,9 +994,10 @@ export default class RealmSettingsPage { cy.findByTestId(this.conditionTypeLink).contains("client-roles").click(); - cy.findByTestId(this.addValue).click(); + cy.get(this.roleSelect).click(); + cy.get(this.roleSelect).contains("create-client").click(); - cy.get('input[name="config.roles[1].value"]').click().type("role 2"); + cy.get(this.roleSelect).click(); cy.findByTestId(this.addConditionSaveBtn).click(); cy.get(this.alertMessage).should( diff --git a/src/client-scopes/messages.ts b/src/client-scopes/messages.ts index 61c195c14a..3f18ceadf8 100644 --- a/src/client-scopes/messages.ts +++ b/src/client-scopes/messages.ts @@ -30,7 +30,6 @@ export default { addMapperExplain: "If you want more fine-grain control, you can create protocol mapper on this client", realmRoles: "Realm roles", - selectARole: "Select a role", clientRoles: "Client roles", selectASourceOfRoles: "Select a source of roles", newRoleName: "New role name", diff --git a/src/components/dynamic/DynamicComponents.tsx b/src/components/dynamic/DynamicComponents.tsx index 516b8a979f..a6f692cc92 100644 --- a/src/components/dynamic/DynamicComponents.tsx +++ b/src/components/dynamic/DynamicComponents.tsx @@ -9,24 +9,13 @@ type DynamicComponentProps = { parentCallback?: (data: string[]) => void; }; -export const DynamicComponents = ({ - properties, - selectedValues, - parentCallback, -}: DynamicComponentProps) => ( +export const DynamicComponents = ({ properties }: DynamicComponentProps) => ( <> {properties.map((property) => { const componentType = property.type!; if (isValidComponentType(componentType)) { const Component = COMPONENTS[componentType]; - return ( - - ); + return ; } else { console.warn(`There is no editor registered for ${componentType}`); } diff --git a/src/components/dynamic/MultivaluedListComponent.tsx b/src/components/dynamic/MultivaluedListComponent.tsx index cde0a76930..809f7c9722 100644 --- a/src/components/dynamic/MultivaluedListComponent.tsx +++ b/src/components/dynamic/MultivaluedListComponent.tsx @@ -48,12 +48,13 @@ export const MultiValuedListComponent = ({ typeAheadAriaLabel="Select" onToggle={(isOpen) => setOpen(isOpen)} selections={value} - onSelect={(_, selectedValue) => { - const option = selectedValue.toString(); - const changedValue = value.includes(option) - ? value.filter((item: string) => item !== option) - : [...value, option]; - onChange(changedValue); + onSelect={(_, v) => { + const option = v.toString(); + if (value.includes(option)) { + onChange(value.filter((item: string) => item !== option)); + } else { + onChange([...value, option]); + } }} onClear={(event) => { event.stopPropagation(); diff --git a/src/components/dynamic/MultivaluedRoleComponent.tsx b/src/components/dynamic/MultivaluedRoleComponent.tsx new file mode 100644 index 0000000000..9d0335652f --- /dev/null +++ b/src/components/dynamic/MultivaluedRoleComponent.tsx @@ -0,0 +1,112 @@ +import React, { useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { sortedUniq } from "lodash"; +import { + FormGroup, + Select, + SelectOption, + SelectVariant, +} from "@patternfly/react-core"; + +import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; +import type { ComponentProps } from "./components"; +import type { MultiLine } from "../multi-line-input/MultiLineInput"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; +import { HelpItem } from "../help-enabler/HelpItem"; +import { convertToHyphens } from "../../util"; +import { useWhoAmI } from "../../context/whoami/WhoAmI"; + +export const MultivaluedRoleComponent = ({ + name, + label, + helpText, +}: ComponentProps) => { + const { t } = useTranslation("dynamic"); + const { whoAmI } = useWhoAmI(); + const fieldName = `config.${convertToHyphens(name!)}`; + + const adminClient = useAdminClient(); + const { control } = useFormContext(); + + const [clientRoles, setClientRoles] = useState([]); + const [open, setOpen] = useState(false); + + useFetch( + async () => { + const clients = await adminClient.clients.find(); + const clientRoles = await Promise.all( + clients.map(async (client) => { + const roles = await adminClient.clients.listRoles({ id: client.id! }); + + return roles.map((role) => ({ + ...role, + })); + }) + ); + + return clientRoles.flat(); + }, + (clientRoles) => { + setClientRoles(clientRoles); + }, + [] + ); + + const alphabetizedClientRoles = sortedUniq( + clientRoles.map((item) => item.name) + ).sort((a, b) => + a!.localeCompare(b!, whoAmI.getLocale(), { ignorePunctuation: true }) + ); + + return ( + + } + fieldId={name!} + > + ( + + )} + /> + + ); +}; diff --git a/src/components/dynamic/components.ts b/src/components/dynamic/components.ts index 17b7bcb0da..f09855ee21 100644 --- a/src/components/dynamic/components.ts +++ b/src/components/dynamic/components.ts @@ -10,10 +10,7 @@ import { ClientSelectComponent } from "./ClientSelectComponent"; import { MultiValuedStringComponent } from "./MultivaluedStringComponent"; import { MultiValuedListComponent } from "./MultivaluedListComponent"; -export type ComponentProps = Omit & { - selectedValues?: string[]; - parentCallback?: (data: any) => void; -}; +export type ComponentProps = Omit; const ComponentTypes = [ "String", "boolean", diff --git a/src/components/dynamic/labels.ts b/src/components/dynamic/labels.ts index fb29ab14a6..7df9034e73 100644 --- a/src/components/dynamic/labels.ts +++ b/src/components/dynamic/labels.ts @@ -1,6 +1,7 @@ export default { dynamic: { addMultivaluedLabel: "Add {{fieldLabel}}", + selectARole: "Select a role", usermodel: { prop: { label: "Property", diff --git a/src/identity-providers/add/AddMapper.tsx b/src/identity-providers/add/AddMapper.tsx index 577f9aa84d..cc4784873d 100644 --- a/src/identity-providers/add/AddMapper.tsx +++ b/src/identity-providers/add/AddMapper.tsx @@ -44,7 +44,7 @@ import { groupBy } from "lodash"; export type IdPMapperRepresentationWithAttributes = IdentityProviderMapperRepresentation & AttributeForm; -type Role = RoleRepresentation & { +export type Role = RoleRepresentation & { clientId?: string; }; diff --git a/src/realm-settings/NewClientPolicyCondition.tsx b/src/realm-settings/NewClientPolicyCondition.tsx index 6558dc99c6..c10fc836a8 100644 --- a/src/realm-settings/NewClientPolicyCondition.tsx +++ b/src/realm-settings/NewClientPolicyCondition.tsx @@ -25,26 +25,33 @@ import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/li import { useRealm } from "../context/realm-context/RealmContext"; import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation"; import type ClientPolicyConditionRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyConditionRepresentation"; -import { DynamicComponents } from "../components/dynamic/DynamicComponents"; import { EditClientPolicyParams, toEditClientPolicy, } from "./routes/EditClientPolicy"; import type { EditClientPolicyConditionParams } from "./routes/EditCondition"; -import { convertToMultiline } from "../components/multi-line-input/MultiLineInput"; +import { + convertToMultiline, + toValue, +} from "../components/multi-line-input/MultiLineInput"; +import { MultivaluedRoleComponent } from "../components/dynamic/MultivaluedRoleComponent"; +import { + COMPONENTS, + isValidComponentType, +} from "../components/dynamic/components"; export type ItemType = { value: string }; +type ConfigProperty = ConfigPropertyRepresentation & { + config: any; +}; + export default function NewClientPolicyCondition() { const { t } = useTranslation("realm-settings"); const { addAlert, addError } = useAlerts(); const history = useHistory(); const { realm } = useRealm(); - const { handleSubmit, control } = useForm({ - mode: "onChange", - }); - const [openConditionType, setOpenConditionType] = useState(false); const [policies, setPolicies] = useState([]); const [condition, setCondition] = useState< @@ -57,13 +64,13 @@ export default function NewClientPolicyCondition() { ConfigPropertyRepresentation[] >([]); - const [selectedVals, setSelectedVals] = useState(); - const { policyName } = useParams(); const { conditionName } = useParams(); const serverInfo = useServerInfo(); - const form = useForm(); + const form = useForm({ + shouldUnregister: false, + }); const conditionTypes = serverInfo.componentTypes?.[ @@ -72,36 +79,20 @@ export default function NewClientPolicyCondition() { const adminClient = useAdminClient(); - const setupForm = (condition: ClientPolicyConditionRepresentation) => { + const setupForm = ( + condition: ClientPolicyConditionRepresentation, + properties: ConfigPropertyRepresentation[] + ) => { form.reset(); - Object.entries(condition).map(([key, value]) => { - if (key === "configuration") { - if ( - conditionName === "client-roles" || - conditionName === "client-updater-source-roles" - ) { - form.setValue("config.roles", convertToMultiline(value["roles"])); - } else if (conditionName === "client-scopes") { - form.setValue("config.scopes", convertToMultiline(value["scopes"])); - form.setValue("config.type", value["type"]); - } else if (conditionName === "client-updater-source-groups") { - form.setValue("config.groups", convertToMultiline(value["groups"])); - } else if (conditionName === "client-updater-source-host") { - form.setValue( - "config.trusted-hosts", - convertToMultiline(value["trusted-hosts"]) - ); - } else if (conditionName === "client-updater-context") { - form.setValue( - "config.update-client-source", - value["update-client-source"][0]["update-client-source"] - ); - } else if (conditionName === "client-access-type") { - form.setValue("config.type", value.type[0]); - } + Object.entries(condition.configuration!).map(([key, value]) => { + const formKey = `config.${key}`; + const property = properties.find((p) => p.name === key); + if (property?.type === "MultivaluedString") { + form.setValue(formKey, convertToMultiline(value)); + } else { + form.setValue(formKey, value); } - form.setValue(key, value); }); }; @@ -123,53 +114,23 @@ export default function NewClientPolicyCondition() { ); setConditionData(typeAndConfigData!); - setSelectedVals(Object.values(typeAndConfigData?.configuration!)[0][0]); setConditionProperties(currentCondition?.properties!); - setupForm(typeAndConfigData!); + setupForm(typeAndConfigData!, currentCondition?.properties!); } }, [] ); - const save = async () => { - const formValues = form.getValues(); - const configValues = formValues.config; + const save = async (configPolicy: ConfigProperty) => { + const configValues = configPolicy.config; - const writeConfig = () => { - if ( - condition[0]?.condition === "any-client" || - conditionName === "any-client" - ) { - return {}; - } else if ( - condition[0]?.condition === "client-access-type" || - conditionName === "client-access-type" - ) { - return { type: [formValues.config.type] }; - } else if ( - condition[0]?.condition === "client-updater-context" || - conditionName === "client-updater-context" - ) { - return { - "update-client-source": [Object.values(formValues)[0]], - }; - } else if ( - condition[0]?.condition === "client-scopes" || - conditionName === "client-scopes" - ) { - return { - type: Object.values(formValues)[0].type, - scopes: (Object.values(formValues)[0].scopes as ItemType[]).map( - (item) => (item as ItemType).value - ), - }; - } else - return { - [Object.keys(configValues)[0]]: Object.values( - configValues?.[Object.keys(configValues)[0]] - ).map((item) => (item as ItemType).value), - }; - }; + const writeConfig = () => + conditionProperties.reduce((r: any, p) => { + p.type === "MultivaluedString" + ? (r[p.name!] = toValue(configValues[p.name!])) + : (r[p.name!] = configValues[p.name!]); + return r; + }, {}); const updatedPolicies = policies.map((policy) => { if (policy.name !== policyName) { @@ -234,10 +195,6 @@ export default function NewClientPolicyCondition() { } }; - const handleCallback = (childData: any) => { - setSelectedVals(childData); - }; - return ( (