diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts index 88ac61a48b..a1ff5e083d 100644 --- a/cypress/integration/realm_settings_test.spec.ts +++ b/cypress/integration/realm_settings_test.spec.ts @@ -566,6 +566,30 @@ describe("Realm settings tests", () => { realmSettingsPage.shouldSearchClientPolicy(); }); + it("Should not have conditions configured by default", () => { + realmSettingsPage.shouldNotHaveConditionsConfigured(); + }); + + it("Should cancel adding a new condition to a client profile", () => { + realmSettingsPage.shouldCancelAddingCondition(); + }); + + it("Should add a new condition to a client profile", () => { + realmSettingsPage.shouldAddCondition(); + }); + + it("Should edit the condition of a client profile", () => { + realmSettingsPage.shouldEditCondition(); + }); + + it("Should cancel deleting condition from a client profile", () => { + realmSettingsPage.shouldCancelDeletingCondition(); + }); + + it("Should delete condition from a client profile", () => { + realmSettingsPage.shouldDeleteCondition(); + }); + it("Check cancelling the client policy deletion", () => { realmSettingsPage.shouldDisplayDeleteClientPolicyDialog(); }); 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 56e1994b50..485204b908 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,12 @@ 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"; private addExecutor = "addExecutor"; private addExecutorDrpDwn = ".pf-c-select__toggle"; @@ -185,6 +189,13 @@ export default class RealmSettingsPage { private addExecutorCancelBtn = "addExecutor-cancelBtn"; private addExecutorSaveBtn = "addExecutor-saveBtn"; private listingPage = new ListingPage(); + private addCondition = "addCondition"; + private addConditionDrpDwn = ".pf-c-select__toggle"; + private addConditionDrpDwnOption = "conditionType-select"; + private addConditionCancelBtn = "addCondition-cancelBtn"; + private addConditionSaveBtn = "addCondition-saveBtn"; + private conditionTypeLink = "condition-type-link"; + private addValue = "addValue"; selectLoginThemeType(themeType: string) { cy.get(this.selectLoginTheme).click(); @@ -843,6 +854,87 @@ export default class RealmSettingsPage { cy.get("table").should("be.visible").contains("td", "Test"); } + shouldNotHaveConditionsConfigured() { + cy.get(this.clientPolicy).click(); + cy.get('h6[class*="kc-emptyConditions"]').should( + "have.text", + "No conditions configured" + ); + } + + shouldCancelAddingCondition() { + cy.get(this.clientPolicy).click(); + cy.findByTestId(this.addCondition).click(); + cy.get(this.addConditionDrpDwn).click(); + cy.findByTestId(this.addConditionDrpDwnOption) + .contains("any-client") + .click(); + cy.findByTestId(this.addConditionCancelBtn).click(); + cy.get('h6[class*="kc-emptyConditions"]').should( + "have.text", + "No conditions configured" + ); + } + + shouldAddCondition() { + cy.get(this.clientPolicy).click(); + cy.findByTestId(this.addCondition).click(); + cy.get(this.addConditionDrpDwn).click(); + cy.findByTestId(this.addConditionDrpDwnOption) + .contains("client-roles") + .click(); + cy.get('input[name="config.roles[0].value"]').click().type("role 1"); + + cy.findByTestId(this.addConditionSaveBtn).click(); + cy.get(this.alertMessage).should( + "be.visible", + "Success! Condition created successfully" + ); + cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles"); + } + + shouldEditCondition() { + cy.get(this.clientPolicy).click(); + + cy.findByTestId(this.conditionTypeLink).contains("client-roles").click(); + + cy.findByTestId(this.addValue).click(); + + cy.get('input[name="config.roles[1].value"]').click().type("role 2"); + + cy.findByTestId(this.addConditionSaveBtn).click(); + cy.get(this.alertMessage).should( + "be.visible", + "Success! Condition updated successfully" + ); + } + + shouldCancelDeletingCondition() { + cy.get(this.clientPolicy).click(); + cy.get('svg[class*="kc-conditionType-trash-icon"]').click(); + cy.get(this.deleteDialogTitle).contains("Delete condition?"); + cy.get(this.deleteDialogBodyText).contains( + "This action will permanently delete client-roles. This cannot be undone." + ); + cy.findByTestId("modalConfirm").contains("Delete"); + cy.get(this.deleteDialogCancelBtn).contains("Cancel").click(); + cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles"); + } + + shouldDeleteCondition() { + cy.get(this.clientPolicy).click(); + cy.get('svg[class*="kc-conditionType-trash-icon"]').click(); + cy.get(this.deleteDialogTitle).contains("Delete condition?"); + cy.get(this.deleteDialogBodyText).contains( + "This action will permanently delete client-roles. This cannot be undone." + ); + cy.findByTestId("modalConfirm").contains("Delete").click(); + cy.get('h6[class*="kc-emptyConditions"]').should( + "have.text", + "No conditions configured" + ); + } + shouldDeleteClientPolicyDialog() { this.listingPage.searchItem("Test", false); this.listingPage.clickRowDetails("Test").clickDetailMenu("Delete"); diff --git a/src/components/dynamic/DynamicComponents.tsx b/src/components/dynamic/DynamicComponents.tsx index 5dd8825ca5..516b8a979f 100644 --- a/src/components/dynamic/DynamicComponents.tsx +++ b/src/components/dynamic/DynamicComponents.tsx @@ -5,15 +5,28 @@ import { COMPONENTS, isValidComponentType } from "./components"; type DynamicComponentProps = { properties: ConfigPropertyRepresentation[]; + selectedValues?: string[]; + parentCallback?: (data: string[]) => void; }; -export const DynamicComponents = ({ properties }: DynamicComponentProps) => ( +export const DynamicComponents = ({ + properties, + selectedValues, + parentCallback, +}: 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 ae5e0f0c5a..f98b3c99a7 100644 --- a/src/components/dynamic/MultivaluedListComponent.tsx +++ b/src/components/dynamic/MultivaluedListComponent.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Controller, useFormContext } from "react-hook-form"; import { @@ -18,10 +18,20 @@ export const MultiValuedListComponent = ({ helpText, defaultValue, options, + selectedValues, + parentCallback, }: ComponentProps) => { const { t } = useTranslation("dynamic"); const { control } = useFormContext(); const [open, setOpen] = useState(false); + const [selectedItems, setSelectedItems] = useState([defaultValue]); + + useEffect(() => { + if (selectedValues) { + parentCallback!(selectedValues); + setSelectedItems(selectedValues!); + } + }, []); return ( item !== option)); + const updatedItems = selectedItems.filter( + (item: string) => item !== option + ); + setSelectedItems(updatedItems); + onChange(updatedItems); + parentCallback!(updatedItems); } else { onChange([...value, option]); + parentCallback!([...value, option]); + setSelectedItems([...selectedItems, option]); } }} onClear={(event) => { diff --git a/src/components/dynamic/components.ts b/src/components/dynamic/components.ts index f09855ee21..17b7bcb0da 100644 --- a/src/components/dynamic/components.ts +++ b/src/components/dynamic/components.ts @@ -10,7 +10,10 @@ import { ClientSelectComponent } from "./ClientSelectComponent"; import { MultiValuedStringComponent } from "./MultivaluedStringComponent"; import { MultiValuedListComponent } from "./MultivaluedListComponent"; -export type ComponentProps = Omit; +export type ComponentProps = Omit & { + selectedValues?: string[]; + parentCallback?: (data: any) => void; +}; const ComponentTypes = [ "String", "boolean", diff --git a/src/components/multi-line-input/MultiLineInput.tsx b/src/components/multi-line-input/MultiLineInput.tsx index 22898b2257..d80ef89f02 100644 --- a/src/components/multi-line-input/MultiLineInput.tsx +++ b/src/components/multi-line-input/MultiLineInput.tsx @@ -15,13 +15,13 @@ export type MultiLine = { }; export function convertToMultiline(fields: string[]): MultiLine[] { - return (fields && fields.length > 0 ? fields : [""]).map((field) => { + return (fields.length > 0 ? fields : [""]).map((field) => { return { value: field }; }); } export function toValue(formValue: MultiLine[]): string[] { - return formValue?.map((field) => field.value); + return formValue.map((field) => field.value); } export type MultiLineInputProps = Omit & { @@ -76,6 +76,8 @@ export const MultiLineInput = ({ onClick={() => append({})} tabIndex={-1} aria-label={t("common:add")} + data-testid="addValue" + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition isDisabled={rest.isDisabled || !currentValues?.[index]?.value} > {t(addButtonLabel || "common:add")} diff --git a/src/realm-settings/NewClientPolicyCondition.tsx b/src/realm-settings/NewClientPolicyCondition.tsx index 5a6f255bae..6558dc99c6 100644 --- a/src/realm-settings/NewClientPolicyCondition.tsx +++ b/src/realm-settings/NewClientPolicyCondition.tsx @@ -23,10 +23,15 @@ import { useAlerts } from "../components/alert/Alerts"; import { useHistory, useParams } from "react-router"; import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation"; import { useRealm } from "../context/realm-context/RealmContext"; -import type { EditClientPolicyParams } from "./routes/EditClientPolicy"; 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"; export type ItemType = { value: string }; @@ -45,12 +50,17 @@ export default function NewClientPolicyCondition() { const [condition, setCondition] = useState< ClientPolicyConditionRepresentation[] >([]); + const [conditionData, setConditionData] = + useState(); const [conditionType, setConditionType] = useState(""); const [conditionProperties, setConditionProperties] = useState< ConfigPropertyRepresentation[] >([]); + const [selectedVals, setSelectedVals] = useState(); + const { policyName } = useParams(); + const { conditionName } = useParams(); const serverInfo = useServerInfo(); const form = useForm(); @@ -62,48 +72,139 @@ export default function NewClientPolicyCondition() { const adminClient = useAdminClient(); + const setupForm = (condition: ClientPolicyConditionRepresentation) => { + 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]); + } + } + form.setValue(key, value); + }); + }; + useFetch( () => adminClient.clientPolicies.listPolicies(), (policies) => { setPolicies(policies.policies ?? []); + if (conditionName) { + const currentPolicy = policies.policies?.find( + (item) => item.name === policyName + ); + + const typeAndConfigData = currentPolicy?.conditions?.find( + (item) => item.condition === conditionName + ); + + const currentCondition = conditionTypes?.find( + (condition) => condition.id === conditionName + ); + + setConditionData(typeAndConfigData!); + setSelectedVals(Object.values(typeAndConfigData?.configuration!)[0][0]); + setConditionProperties(currentCondition?.properties!); + setupForm(typeAndConfigData!); + } }, [] ); const save = async () => { + const formValues = form.getValues(); + const configValues = formValues.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 updatedPolicies = policies.map((policy) => { if (policy.name !== policyName) { return policy; } - const formValues = form.getValues(); - const configValues = formValues.config; + let conditions = policy.conditions ?? []; - const writeConfig = () => { - if (condition[0]?.condition === "any-client") { - return {}; - } else if (condition[0]?.condition === "client-access-type") { - return { type: [formValues["client-accesstype"].label] }; - } else if (condition[0]?.condition === "client-updater-context") { - return { - "update-client-source": [Object.values(formValues)[0]], - }; - } else if (condition[0]?.condition === "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), - }; - }; + if (conditionName) { + const createdCondition = { + condition: conditionData?.condition, + configuration: writeConfig(), + }; - const conditions = (policy.conditions ?? []).concat({ + const index = conditions.findIndex( + (condition) => conditionName === condition.condition + ); + + if (index === -1) { + return; + } + + const newConditions = [ + ...conditions.slice(0, index), + createdCondition, + ...conditions.slice(index + 1), + ]; + + return { + ...policy, + conditions: newConditions, + }; + } + + conditions = conditions.concat({ condition: condition[0].condition, configuration: writeConfig(), }); @@ -112,7 +213,7 @@ export default function NewClientPolicyCondition() { ...policy, conditions, }; - }); + }) as ClientPolicyRepresentation[]; try { await adminClient.clientPolicies.updatePolicy({ @@ -123,7 +224,9 @@ export default function NewClientPolicyCondition() { `/${realm}/realm-settings/clientPolicies/${policyName}/edit-policy` ); addAlert( - t("realm-settings:createClientConditionSuccess"), + conditionName + ? t("realm-settings:updateClientConditionSuccess") + : t("realm-settings:createClientConditionSuccess"), AlertVariant.success ); } catch (error) { @@ -131,9 +234,16 @@ export default function NewClientPolicyCondition() { } }; + const handleCallback = (childData: any) => { + setSelectedVals(childData); + }; + return ( - + (