Create client policy form (#1309)

* add create client policy form; WIP

add client policy tests

checkout realm settings test from master

RealmSettingsPage.ts master

remove comment and add missing translation

fix tests

PR feedback from Jon and Erik

* rebase

* update test file

* replace hardcoded URL with path

* fix tests

* correct tab name

* fix duplicates test
This commit is contained in:
Jenny 2021-10-12 11:03:56 -04:00 committed by GitHub
parent 60e4676ac8
commit 001d4ed6d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 416 additions and 31 deletions

View file

@ -259,20 +259,20 @@ describe("Realm settings tests", () => {
}); });
/*it("delete providers", () => { /*it("delete providers", () => {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
const url = `/auth/admin/realms/${realmName}/keys`; const url = `/auth/admin/realms/${realmName}/keys`;
cy.intercept(url).as("load"); cy.intercept(url).as("load");
cy.findByTestId("rs-keys-tab").click(); cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click(); cy.findByTestId("rs-providers-tab").click();
cy.wait("@load"); cy.wait("@load");
deleteProvider("test_aes-generated"); deleteProvider("test_aes-generated");
deleteProvider("test_ecdsa-generated"); deleteProvider("test_ecdsa-generated");
deleteProvider("test_hmac-generated"); deleteProvider("test_hmac-generated");
deleteProvider("test_rsa-generated"); deleteProvider("test_rsa-generated");
});*/ });*/
it("Test keys", () => { it("Test keys", () => {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
goToKeys(); goToKeys();
@ -443,7 +443,7 @@ describe("Realm settings tests", () => {
loginPage.logIn(); loginPage.logIn();
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
cy.findByTestId("rs-clientPolicies-tab").click(); cy.findByTestId("rs-clientPolicies-tab").click();
cy.findByTestId("rs-profiles-clientPolicies-tab").click(); cy.findByTestId("rs-policies-clientProfiles-tab").click();
}); });
it("Go to client policies profiles tab", () => { it("Go to client policies profiles tab", () => {
@ -484,18 +484,11 @@ describe("Realm settings tests", () => {
}); });
it("Should not create duplicate client profile", () => { it("Should not create duplicate client profile", () => {
realmSettingsPage.shouldCompleteAndCreateNewClientProfile();
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
cy.findByTestId("rs-clientPolicies-tab").click(); cy.findByTestId("rs-clientPolicies-tab").click();
cy.findByTestId("rs-profiles-clientPolicies-tab").click(); cy.findByTestId("rs-policies-clientProfiles-tab").click();
realmSettingsPage.shouldCompleteAndCreateNewClientProfile(); realmSettingsPage.shouldCompleteAndCreateNewClientProfile();
realmSettingsPage.shouldNotCreateDuplicateClientProfile(); realmSettingsPage.shouldNotCreateDuplicateClientProfile();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-clientPolicies-tab").click();
cy.findByTestId("rs-profiles-clientPolicies-tab").click();
realmSettingsPage.shouldDeleteClientProfileDialog();
}); });
it("Check deleting newly created client profile from create view via dropdown", () => { it("Check deleting newly created client profile from create view via dropdown", () => {

View file

@ -0,0 +1,280 @@
import React, { useState } from "react";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
Divider,
DropdownItem,
Flex,
FlexItem,
FormGroup,
PageSection,
Text,
TextArea,
TextInput,
TextVariants,
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { FormAccess } from "../components/form-access/FormAccess";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { Link, useHistory } from "react-router-dom";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { PlusCircleIcon } from "@patternfly/react-icons";
import "./RealmSettingsSection.css";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyRepresentation";
import { toClientPolicies } from "./routes/ClientPolicies";
type NewClientPolicyForm = Required<ClientPolicyRepresentation>;
const defaultValues: NewClientPolicyForm = {
name: "",
description: "",
conditions: [],
enabled: true,
profiles: [],
};
export const NewClientPolicyForm = () => {
const { t } = useTranslation("realm-settings");
const {
register,
errors,
handleSubmit,
reset: resetForm,
} = useForm<NewClientPolicyForm>({
defaultValues,
});
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const adminClient = useAdminClient();
const [policies, setPolicies] = useState<ClientProfileRepresentation[]>([]);
const [
showAddConditionsAndProfilesForm,
setShowAddConditionsAndProfilesForm,
] = useState(false);
const [createdPolicy, setCreatedPolicy] =
useState<ClientPolicyRepresentation>();
const history = useHistory();
useFetch(
() => adminClient.clientPolicies.listPolicies(),
(policies) => {
setPolicies(policies.policies ?? []);
},
[]
);
const save = async (form: NewClientPolicyForm) => {
const createdPolicy = {
...form,
profiles: [],
conditions: [],
};
const allPolicies = policies.concat(createdPolicy);
try {
await adminClient.clientPolicies.updatePolicy({
policies: allPolicies,
});
addAlert(
t("realm-settings:createClientPolicySuccess"),
AlertVariant.success
);
setShowAddConditionsAndProfilesForm(true);
setCreatedPolicy(createdPolicy);
} catch (error) {
addError("realm-settings:createClientProfileError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientProfileConfirmTitle"),
messageKey: t("deleteClientProfileConfirm"),
continueButtonLabel: t("delete"),
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
const updatedPolicies = policies.filter(
(policy) => policy.name !== createdPolicy?.name
);
try {
await adminClient.clientPolicies.updatePolicy({
policies: updatedPolicies,
});
addAlert(t("deleteClientSuccess"), AlertVariant.success);
history.push(toClientPolicies({ realm }));
} catch (error) {
addError(t("deleteClientError"), error);
}
},
});
return (
<>
<DeleteConfirm />
<ViewHeader
titleKey={
showAddConditionsAndProfilesForm
? createdPolicy?.name!
: t("createPolicy")
}
divider
dropdownItems={
showAddConditionsAndProfilesForm
? [
<DropdownItem
key="delete"
value="delete"
onClick={() => {
toggleDeleteDialog;
}}
data-testid="deleteClientProfileDropdown"
>
{t("deleteClientProfile")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light">
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
<FormGroup
label={t("common:name")}
fieldId="kc-name"
isRequired
helperTextInvalid={t("common:required")}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
>
<TextInput
ref={register({ required: true })}
type="text"
id="kc-client-profile-name"
name="name"
data-testid="client-policy-name"
/>
</FormGroup>
<FormGroup label={t("common:description")} fieldId="kc-description">
<TextArea
name="description"
aria-label={t("description")}
ref={register()}
type="text"
id="kc-client-policy-description"
data-testid="client-policy-description"
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={handleSubmit(save)}
data-testid="saveCreatePolicy"
isDisabled={showAddConditionsAndProfilesForm}
>
{t("common:save")}
</Button>
<Button
id="cancelCreatePolicy"
onClick={() =>
showAddConditionsAndProfilesForm
? resetForm(createdPolicy)
: history.push(toClientPolicies({ realm }))
}
data-testid="cancelCreatePolicy"
>
{showAddConditionsAndProfilesForm
? t("realm-settings:reload")
: t("common:cancel")}
</Button>
</ActionGroup>
{showAddConditionsAndProfilesForm && (
<>
<Flex>
<FlexItem>
<Text className="kc-conditions" component={TextVariants.h1}>
{t("conditions")}
<HelpItem
helpText={t("realm-settings-help:conditions")}
forLabel={t("conditionsHelpItem")}
forID={t("conditions")}
/>
</Text>
</FlexItem>
<FlexItem align={{ default: "alignRight" }}>
<Button
id="addCondition"
component={(props) => (
<Link
{...props}
to={`/${realm}/realm-settings/clientPolicies`}
></Link>
)}
variant="link"
className="kc-addCondition"
data-testid="cancelCreateProfile"
icon={<PlusCircleIcon />}
>
{t("realm-settings:addCondition")}
</Button>
</FlexItem>
</Flex>
<Divider />
<Text className="kc-emptyConditions" component={TextVariants.h6}>
{t("realm-settings:emptyConditions")}
</Text>
</>
)}
{showAddConditionsAndProfilesForm && (
<>
<Flex>
<FlexItem>
<Text
className="kc-client-profiles"
component={TextVariants.h1}
>
{t("clientProfiles")}
<HelpItem
helpText={t("realm-settings-help:clientProfiles")}
forLabel={t("clientProfilesHelpItem")}
forID={t("clientProfiles")}
/>
</Text>
</FlexItem>
<FlexItem align={{ default: "alignRight" }}>
<Button
id="addExecutor"
variant="link"
className="kc-addClientProfile"
data-testid="cancelCreateProfile"
icon={<PlusCircleIcon />}
>
{t("realm-settings:addClientProfile")}
</Button>
</FlexItem>
</Flex>
<Divider />
<Text
className="kc-emptyClientProfiles"
component={TextVariants.h6}
>
{t("realm-settings:emptyProfiles")}
</Text>
</>
)}
</FormAccess>
</PageSection>
</>
);
};

View file

@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react"; import React, { useState } from "react";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -19,32 +19,68 @@ import { useTranslation } from "react-i18next";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { prettyPrintJSON, upperCaseFormatter } from "../util"; import { prettyPrintJSON, upperCaseFormatter } from "../util";
import { CodeEditor, Language } from "@patternfly/react-code-editor"; import { CodeEditor, Language } from "@patternfly/react-code-editor";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyRepresentation"; import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyRepresentation";
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 "./RealmSettingsSection.css"; import "./RealmSettingsSection.css";
import { useRealm } from "../context/realm-context/RealmContext";
import { toNewClientPolicy } from "./routes/NewClientPolicy";
export const PoliciesTab = () => { export const PoliciesTab = () => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { realm } = useRealm();
const history = useHistory();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>(); const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>();
const [selectedPolicy, setSelectedPolicy] = const [selectedPolicy, setSelectedPolicy] =
useState<ClientPolicyRepresentation>(); useState<ClientPolicyRepresentation>();
const [key, setKey] = useState(0);
const [code, setCode] = useState<string>();
const [tablePolicies, setTablePolicies] =
useState<ClientPolicyRepresentation[]>();
const refresh = () => setKey(key + 1);
useFetch( useFetch(
() => adminClient.clientPolicies.listPolicies(), () => adminClient.clientPolicies.listPolicies(),
(policies) => setPolicies(policies.policies), (policies) => {
[] setPolicies(policies.policies),
setTablePolicies(policies.policies || []),
setCode(prettyPrintJSON(policies.policies));
},
[key]
); );
const loader = async () => policies ?? []; const loader = async () => policies ?? [];
const code = useMemo(() => prettyPrintJSON(policies), [policies]); const save = async () => {
if (!code) {
return;
}
try {
const obj: ClientPolicyRepresentation[] = JSON.parse(code);
try {
await adminClient.clientPolicies.updatePolicy({
policies: obj,
});
addAlert(
t("realm-settings:updateClientPoliciesSuccess"),
AlertVariant.success
);
refresh();
} catch (error) {
addError("realm-settings:updateClientPoliciesError", error);
}
} catch (error) {
console.warn("Invalid json, ignoring value using {}");
addError("realm-settings:updateClientPoliciesError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientPolicyConfirmTitle"), titleKey: t("deleteClientPolicyConfirmTitle"),
@ -54,9 +90,16 @@ export const PoliciesTab = () => {
continueButtonLabel: t("delete"), continueButtonLabel: t("delete"),
continueButtonVariant: ButtonVariant.danger, continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => { onConfirm: async () => {
const updatedPolicies = policies?.filter(
(policy) => policy.name !== selectedPolicy?.name
);
try { try {
// delete client policy here await adminClient.clientPolicies.updatePolicy({
policies: updatedPolicies,
});
addAlert(t("deleteClientPolicySuccess"), AlertVariant.success); addAlert(t("deleteClientPolicySuccess"), AlertVariant.success);
refresh();
} catch (error) { } catch (error) {
addError(t("deleteClientPolicyError"), error); addError(t("deleteClientPolicyError"), error);
} }
@ -87,6 +130,7 @@ export const PoliciesTab = () => {
onChange={() => setShow(false)} onChange={() => setShow(false)}
label={t("policiesConfigTypes.formView")} label={t("policiesConfigTypes.formView")}
id="formView-policiesView" id="formView-policiesView"
data-testid="formView-policiesView"
className="kc-form-radio-btn pf-u-mr-sm pf-u-ml-sm" className="kc-form-radio-btn pf-u-mr-sm pf-u-ml-sm"
/> />
</FlexItem> </FlexItem>
@ -97,6 +141,7 @@ export const PoliciesTab = () => {
onChange={() => setShow(true)} onChange={() => setShow(true)}
label={t("policiesConfigTypes.jsonEditor")} label={t("policiesConfigTypes.jsonEditor")}
id="jsonEditor-policiesView" id="jsonEditor-policiesView"
data-testid="jsonEditor-policiesView"
className="kc-editor-radio-btn" className="kc-editor-radio-btn"
/> />
</FlexItem> </FlexItem>
@ -111,6 +156,7 @@ export const PoliciesTab = () => {
message={t("realm-settings:noClientPolicies")} message={t("realm-settings:noClientPolicies")}
instructions={t("realm-settings:noClientPoliciesInstructions")} instructions={t("realm-settings:noClientPoliciesInstructions")}
primaryActionText={t("realm-settings:createClientPolicy")} primaryActionText={t("realm-settings:createClientPolicy")}
onPrimaryAction={() => history.push(toNewClientPolicy({ realm }))}
/> />
} }
ariaLabelKey="realm-settings:clientPolicies" ariaLabelKey="realm-settings:clientPolicies"
@ -120,7 +166,9 @@ export const PoliciesTab = () => {
<ToolbarItem> <ToolbarItem>
<Button <Button
id="createPolicy" id="createPolicy"
component={Link} component={(props) => (
<Link {...props} to={toNewClientPolicy({ realm })} />
)}
data-testid="createPolicy" data-testid="createPolicy"
> >
{t("createClientPolicy")} {t("createClientPolicy")}
@ -155,19 +203,34 @@ export const PoliciesTab = () => {
<CodeEditor <CodeEditor
isLineNumbersVisible isLineNumbersVisible
isLanguageLabelVisible isLanguageLabelVisible
isReadOnly={false}
code={code} code={code}
language={Language.json} language={Language.json}
height="30rem" height="30rem"
onChange={(value) => {
setCode(value ?? "");
}}
/> />
</div> </div>
<div className="pf-u-mt-md"> <div className="pf-u-mt-md">
<Button <Button
variant={ButtonVariant.primary} variant={ButtonVariant.primary}
className="pf-u-mr-md pf-u-ml-lg" className="pf-u-mr-md pf-u-ml-lg"
data-testid="jsonEditor-policies-saveBtn"
onClick={save}
> >
{t("save")} {t("save")}
</Button> </Button>
<Button variant={ButtonVariant.secondary}> {t("reload")}</Button> <Button
variant={ButtonVariant.secondary}
data-testid="jsonEditor-reloadBtn"
onClick={() => {
setCode(prettyPrintJSON(tablePolicies));
}}
>
{" "}
{t("reload")}
</Button>
</div> </div>
</> </>
)} )}

View file

@ -198,6 +198,14 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
color: #8D9195; color: #8D9195;
} }
.kc-emptyConditions {
color: #8D9195;
}
.kc-emptyClientProfiles {
color: #8D9195;
}
.kc-action-dropdown { .kc-action-dropdown {
background-color: transparent; background-color: transparent;
} }

View file

@ -377,7 +377,7 @@ export const RealmSettingsTabs = ({
<Tab <Tab
id="profiles" id="profiles"
eventKey={0} eventKey={0}
data-testid="rs-profiles-clientPolicies-tab" data-testid="rs-policies-clientProfiles-tab"
aria-label={t("clientProfilesSubTab")} aria-label={t("clientProfilesSubTab")}
title={ title={
<TabTitleText> <TabTitleText>

View file

@ -112,5 +112,8 @@ export default {
"The locales to support for this realm. The user chooses one of these locales on the login screen.", "The locales to support for this realm. The user chooses one of these locales on the login screen.",
defaultLocale: defaultLocale:
"The initial locale to use. It is used on the login screen and other screens in the Admin Console and Account Console.", "The initial locale to use. It is used on the login screen and other screens in the Admin Console and Account Console.",
conditions:
"Conditions, which will be evaluated to determine if client policy should be applied during particular action or not.",
clientProfiles: "Client profiles applied on this policy.",
}, },
}; };

View file

@ -194,7 +194,9 @@ export default {
noClientPolicies: "No client policies", noClientPolicies: "No client policies",
noClientPoliciesInstructions: noClientPoliciesInstructions:
"There are no client policies. Select 'Create client policy' to create a new client policy.", "There are no client policies. Select 'Create client policy' to create a new client policy.",
createPolicy: "Create policy",
createClientPolicy: "Create client policy", createClientPolicy: "Create client policy",
createClientPolicySuccess: "New policy created",
clientPolicySearch: "Search client policy", clientPolicySearch: "Search client policy",
policiesConfigType: "Configure via:", policiesConfigType: "Configure via:",
policiesConfigTypes: { policiesConfigTypes: {
@ -257,6 +259,21 @@ export default {
"The client profiles configuration was updated", "The client profiles configuration was updated",
updateClientProfilesError: updateClientProfilesError:
"Provided JSON is incorrect: Unexpected token { in JSON", "Provided JSON is incorrect: Unexpected token { in JSON",
conditions: "Conditions",
conditionsHelpItem: "Conditions help item",
addCondition: "Add condition",
emptyConditions: "No conditions configured",
updateClientPoliciesSuccess:
"The client policies configuration was updated",
updateClientPoliciesError:
"Provided JSON is incorrect: Unexpected token { in JSON",
clientProfiles: "Client profiles",
clientProfilesHelpItem: "Client profiles help item",
addClientProfile: "Add client profile",
emptyProfiles: "No client profiles configured",
tokens: "Tokens", tokens: "Tokens",
key: "Key", key: "Key",
value: "Value", value: "Value",

View file

@ -8,6 +8,7 @@ import { RsaGeneratedSettingsRoute } from "./routes/RsaGeneratedSettings";
import { RsaSettingsRoute } from "./routes/RsaSettings"; import { RsaSettingsRoute } from "./routes/RsaSettings";
import { ClientPoliciesRoute } from "./routes/ClientPolicies"; import { ClientPoliciesRoute } from "./routes/ClientPolicies";
import { NewClientProfileRoute } from "./routes/NewClientProfile"; import { NewClientProfileRoute } from "./routes/NewClientProfile";
import { NewClientPolicyRoute } from "./routes/NewClientPolicy";
const routes: RouteDef[] = [ const routes: RouteDef[] = [
RealmSettingsRoute, RealmSettingsRoute,
@ -19,6 +20,7 @@ const routes: RouteDef[] = [
RsaSettingsRoute, RsaSettingsRoute,
ClientPoliciesRoute, ClientPoliciesRoute,
NewClientProfileRoute, NewClientProfileRoute,
NewClientPolicyRoute,
]; ];
export default routes; export default routes;

View file

@ -0,0 +1,19 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { NewClientPolicyForm } from "../NewClientPolicyForm";
export type NewClientPolicyParams = { realm: string };
export const NewClientPolicyRoute: RouteDef = {
path: "/:realm/realm-settings/clientPolicies/new-client-policy",
component: NewClientPolicyForm,
breadcrumb: (t) => t("realm-settings:createPolicy"),
access: "manage-realm",
};
export const toNewClientPolicy = (
params: NewClientPolicyParams
): LocationDescriptorObject => ({
pathname: generatePath(NewClientPolicyRoute.path, params),
});