Added Security Defenses tab in the realm settings (#738)
* initial version of the Security defenses tab * disable when not dirty * added test * removed unsessary fetches * fixed format * fixed tests * fixed title * Update src/realm-settings/security-defences/BruteForceDetection.tsx Co-authored-by: Jenny <32821331+jenny-s51@users.noreply.github.com> * Update src/realm-settings/messages.json Co-authored-by: Jenny <32821331+jenny-s51@users.noreply.github.com> * fixed test Co-authored-by: Jenny <32821331+jenny-s51@users.noreply.github.com>
This commit is contained in:
parent
b0d6ad9356
commit
dfda67146c
14 changed files with 488 additions and 72 deletions
|
@ -31,18 +31,17 @@ describe("Realm settings", () => {
|
|||
});
|
||||
|
||||
const goToKeys = () => {
|
||||
const keysUrl = "/auth/admin/realms/master/keys";
|
||||
const keysUrl = `/auth/admin/realms/${realmName}/keys`;
|
||||
cy.intercept(keysUrl).as("keysFetch");
|
||||
cy.getId("rs-keys-tab").click();
|
||||
cy.wait(10000);
|
||||
cy.getId("rs-keys-list-tab").click();
|
||||
cy.wait(["@keysFetch"]);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
const addBundle = () => {
|
||||
const localizationUrl =
|
||||
"/auth/admin/realms/master/realm-settings/localization";
|
||||
const localizationUrl = `/auth/admin/realms/${realmName}/localization/en`;
|
||||
cy.intercept(localizationUrl).as("localizationFetch");
|
||||
|
||||
realmSettingsPage.addKeyValuePair(
|
||||
|
@ -50,6 +49,8 @@ describe("Realm settings", () => {
|
|||
"value_" + (Math.random() + 1).toString(36).substring(7)
|
||||
);
|
||||
|
||||
cy.wait(["@localizationFetch"]);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -57,8 +58,10 @@ describe("Realm settings", () => {
|
|||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch);
|
||||
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
|
||||
masthead.checkNotificationMessage("Realm successfully updated");
|
||||
realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch);
|
||||
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
|
||||
masthead.checkNotificationMessage("Realm successfully updated");
|
||||
});
|
||||
|
||||
it("Go to login tab", () => {
|
||||
|
@ -96,9 +99,12 @@ describe("Realm settings", () => {
|
|||
});
|
||||
|
||||
it("Go to themes tab", () => {
|
||||
cy.wait(5000);
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.intercept(`/auth/admin/realms/${realmName}/keys`).as("load");
|
||||
|
||||
cy.getId("rs-themes-tab").click();
|
||||
cy.wait(["@load"]);
|
||||
|
||||
realmSettingsPage.selectLoginThemeType("keycloak");
|
||||
realmSettingsPage.selectAccountThemeType("keycloak");
|
||||
realmSettingsPage.selectAdminThemeType("base");
|
||||
|
@ -111,10 +117,11 @@ describe("Realm settings", () => {
|
|||
const listingPage = new ListingPage();
|
||||
|
||||
it("Enable user events", () => {
|
||||
cy.intercept("GET", `/auth/admin/realms/${realmName}/keys`).as("load");
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.getId("rs-realm-events-tab").click();
|
||||
cy.wait(["@load"]);
|
||||
|
||||
cy.wait(5000);
|
||||
realmSettingsPage
|
||||
.toggleSwitch(realmSettingsPage.enableEvents)
|
||||
.save(realmSettingsPage.eventsUserSave);
|
||||
|
@ -147,8 +154,6 @@ describe("Realm settings", () => {
|
|||
});
|
||||
|
||||
it("Go to keys tab", () => {
|
||||
cy.wait(5000);
|
||||
|
||||
sidebarPage.goToRealmSettings();
|
||||
|
||||
cy.getId("rs-keys-tab").click();
|
||||
|
@ -204,4 +209,13 @@ describe("Realm settings", () => {
|
|||
"Success! The localization text has been created."
|
||||
);
|
||||
});
|
||||
|
||||
it("Realm header settings", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.get("#pf-tab-securityDefences-securityDefences").click();
|
||||
cy.getId("headers-form-tab-save").should("be.disabled");
|
||||
cy.get("#xFrameOptions").clear().type("DENY");
|
||||
cy.getId("headers-form-tab-save").should("be.enabled").click();
|
||||
masthead.checkNotificationMessage("Realm successfully updated");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,9 +45,6 @@ export default class RealmSettingsPage {
|
|||
valueInput = "value-input";
|
||||
|
||||
selectLoginThemeType(themeType: string) {
|
||||
const themesUrl = "/auth/admin/realms/master/themes";
|
||||
cy.intercept(themesUrl).as("themesFetch");
|
||||
|
||||
cy.get(this.selectLoginTheme).click();
|
||||
cy.get(this.loginThemeList).contains(themeType).click();
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useContext } from "react";
|
||||
import React, { isValidElement, ReactNode, useContext } from "react";
|
||||
import { Popover } from "@patternfly/react-core";
|
||||
import { HelpIcon } from "@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpContext } from "./HelpHeader";
|
||||
|
||||
type HelpItemProps = {
|
||||
helpText: string;
|
||||
helpText: string | ReactNode;
|
||||
forLabel: string;
|
||||
forID: string;
|
||||
noVerticalAlign?: boolean;
|
||||
|
@ -26,7 +26,11 @@ export const HelpItem = ({
|
|||
return (
|
||||
<>
|
||||
{enabled && (
|
||||
<Popover bodyContent={t(helpText)}>
|
||||
<Popover
|
||||
bodyContent={
|
||||
isValidElement(helpText) ? helpText : t(helpText as string)
|
||||
}
|
||||
>
|
||||
<>
|
||||
{!unWrap && (
|
||||
<button
|
||||
|
|
|
@ -62,7 +62,7 @@ export const RealmSettingsEmailTab = ({
|
|||
const authenticationEnabled = useWatch({
|
||||
control,
|
||||
name: "smtpServer.authentication",
|
||||
defaultValue: realm?.smtpServer!.authentication,
|
||||
defaultValue: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -36,7 +36,12 @@ export const RealmSettingsGeneralTab = ({
|
|||
const { t } = useTranslation("realm-settings");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm: realmName } = useRealm();
|
||||
const { register, control, handleSubmit } = useFormContext();
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isDirty },
|
||||
} = useFormContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const baseUrl = getBaseUrl(adminClient);
|
||||
|
@ -193,6 +198,7 @@ export const RealmSettingsGeneralTab = ({
|
|||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="general-tab-save"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
|
|
|
@ -34,6 +34,7 @@ import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
|||
import { LocalizationTab } from "./LocalizationTab";
|
||||
import { WhoAmIContext } from "../context/whoami/WhoAmI";
|
||||
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import { SecurityDefences } from "./security-defences/SecurityDefences";
|
||||
|
||||
type RealmSettingsHeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
|
@ -130,7 +131,7 @@ export const RealmSettingsSection = () => {
|
|||
const adminClient = useAdminClient();
|
||||
const { realm: realmName } = useRealm();
|
||||
const { addAlert } = useAlerts();
|
||||
const form = useForm();
|
||||
const form = useForm({ mode: "onChange" });
|
||||
const { control, getValues, setValue, reset: resetForm } = form;
|
||||
const [key, setKey] = useState(0);
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
|
@ -143,35 +144,6 @@ export const RealmSettingsSection = () => {
|
|||
const kpComponentTypes =
|
||||
useServerInfo().componentTypes!["org.keycloak.keys.KeyProvider"];
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
setupForm(realm);
|
||||
setRealm(realm);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useFetch(
|
||||
() => adminClient.users.findOne({ id: whoAmI.getUserId()! }),
|
||||
|
||||
(user) => {
|
||||
setCurrentUser(user);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const update = async () => {
|
||||
const realmComponents = await adminClient.components.find({
|
||||
type: "org.keycloak.keys.KeyProvider",
|
||||
realm: realmName,
|
||||
});
|
||||
setRealmComponents(realmComponents);
|
||||
};
|
||||
setTimeout(update, 100);
|
||||
}, [key]);
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const realm = await adminClient.realms.findOne({ realm: realmName });
|
||||
|
@ -179,12 +151,14 @@ export const RealmSettingsSection = () => {
|
|||
type: "org.keycloak.keys.KeyProvider",
|
||||
realm: realmName,
|
||||
});
|
||||
const user = await adminClient.users.findOne({ id: whoAmI.getUserId()! });
|
||||
|
||||
return { realm, realmComponents };
|
||||
return { user, realm, realmComponents };
|
||||
},
|
||||
(result) => {
|
||||
setRealm(result.realm);
|
||||
setRealmComponents(result.realmComponents);
|
||||
({ user, realm, realmComponents }) => {
|
||||
setRealmComponents(realmComponents);
|
||||
setCurrentUser(user);
|
||||
setRealm(realm);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
@ -194,13 +168,11 @@ export const RealmSettingsSection = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (realm) setupForm(realm);
|
||||
}, [realm]);
|
||||
|
||||
const setupForm = (realm: RealmRepresentation) => {
|
||||
resetForm(realm);
|
||||
Object.entries(realm).map((entry) => setValue(entry[0], entry[1]));
|
||||
};
|
||||
if (realm) {
|
||||
Object.entries(realm).map((entry) => setValue(entry[0], entry[1]));
|
||||
resetForm({ ...realm });
|
||||
}
|
||||
}, [realm, resetForm]);
|
||||
|
||||
const save = async (realm: RealmRepresentation) => {
|
||||
try {
|
||||
|
@ -240,7 +212,7 @@ export const RealmSettingsSection = () => {
|
|||
>
|
||||
<RealmSettingsGeneralTab
|
||||
save={save}
|
||||
reset={() => setupForm(realm!)}
|
||||
reset={() => resetForm(realm!)}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
|
@ -266,7 +238,7 @@ export const RealmSettingsSection = () => {
|
|||
>
|
||||
<RealmSettingsThemesTab
|
||||
save={save}
|
||||
reset={() => setupForm(realm!)}
|
||||
reset={() => resetForm(realm!)}
|
||||
realm={realm!}
|
||||
/>
|
||||
</Tab>
|
||||
|
@ -311,22 +283,31 @@ export const RealmSettingsSection = () => {
|
|||
<EventsTab />
|
||||
</Tab>
|
||||
|
||||
{realm && (
|
||||
<Tab
|
||||
id="localization"
|
||||
eventKey="localization"
|
||||
data-testid="rs-localization-tab"
|
||||
title={<TabTitleText>{t("localization")}</TabTitleText>}
|
||||
>
|
||||
<Tab
|
||||
id="localization"
|
||||
eventKey="localization"
|
||||
data-testid="rs-localization-tab"
|
||||
title={<TabTitleText>{t("localization")}</TabTitleText>}
|
||||
>
|
||||
{realm && (
|
||||
<LocalizationTab
|
||||
key={key}
|
||||
refresh={refresh}
|
||||
save={save}
|
||||
reset={() => setupForm(realm)}
|
||||
reset={() => resetForm(realm)}
|
||||
realm={realm}
|
||||
/>
|
||||
</Tab>
|
||||
)}
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
id="securityDefences"
|
||||
eventKey="securityDefences"
|
||||
title={<TabTitleText>{t("securityDefences")}</TabTitleText>}
|
||||
>
|
||||
{realm && (
|
||||
<SecurityDefences save={save} reset={() => resetForm(realm)} />
|
||||
)}
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
|
|
|
@ -31,7 +31,20 @@
|
|||
"keyAlias": "Alias for the private key",
|
||||
"keyPassword": "Password for the private key",
|
||||
"privateRSAKey": "Private RSA Key encoded in PEM format",
|
||||
"x509Certificate": "X509 Certificate encoded in PEM format"
|
||||
|
||||
"x509Certificate": "X509 Certificate encoded in PEM format",
|
||||
"xFrameOptions": "Default value prevents pages from being included by non-origin iframes <1>Learn more</1>",
|
||||
"contentSecurityPolicy": "Default value prevents pages from being included by non-origin iframes <1>Learn more</1>",
|
||||
"contentSecurityPolicyReportOnly": "For testing Content Security Policies <1>Learn more</1>",
|
||||
"xContentTypeOptions": "Default value prevents Internet Explorer and Google Chrome from MIME-sniffing a response away from the declared content-type <1>Learn more</1>",
|
||||
"xRobotsTag": "Prevent pages from appearing in search engines <1>Learn more</1>",
|
||||
"xXSSProtection": "Prevent pages from appearing in search engines <1>Learn more</1>",
|
||||
"strictTransportSecurity": "The Strict-Transport-Security HTTP header tells browsers to always use HTTPS. Once a browser sees this header, it will only visit the site over HTTPS for the time specified (1 year) at max-age, including the subdomains. <1>Learn more</1>",
|
||||
"failureFactor": "How many failures before wait is triggered.",
|
||||
"permanentLockout": "Lock the user permanently when the user exceeds the maximum login failures.",
|
||||
"waitIncrement": "When failure threshold has been met, how much time should the user be locked out?",
|
||||
"maxFailureWait": "Max time a user will be locked out.",
|
||||
"maxDeltaTime": "When will failure count be reset?",
|
||||
"quickLoginCheckMilliSeconds": "If a failure happens concurrently too quickly, lock out the user.",
|
||||
"minimumQuickLoginWait": "How long to wait after a quick login failure."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -505,7 +505,26 @@
|
|||
"confirm": "Confirm",
|
||||
"noMessageBundles": "No message bundles",
|
||||
"noMessageBundlesInstructions": "Add a message bundle to get started.",
|
||||
"messageBundleDescription": "You can edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale."
|
||||
"messageBundleDescription": "You can edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale.",
|
||||
"defaultRoles": "Default roles",
|
||||
"defaultGroups": "Default groups",
|
||||
"securityDefences": "Security defenses",
|
||||
"headers": "Headers",
|
||||
"bruteForceDetection": "Brute force detection",
|
||||
"xFrameOptions": "X-Frame-Options",
|
||||
"contentSecurityPolicy": "Content-Security-Policy",
|
||||
"contentSecurityPolicyReportOnly": "Content-Security-Policy-Report-Only",
|
||||
"xContentTypeOptions": "X-Content-Type-Options",
|
||||
"xRobotsTag": "X-Robots-Tag",
|
||||
"xXSSProtection": "X-XSS-Protection",
|
||||
"strictTransportSecurity": "HTTP Strict Transport Security (HSTS)",
|
||||
"failureFactor": "Max login failures",
|
||||
"permanentLockout": "Permanent lockout",
|
||||
"waitIncrement": "Wait increment",
|
||||
"maxFailureWait": "Max wait",
|
||||
"maxDeltaTime": "Failure reset time",
|
||||
"quickLoginCheckMilliSeconds": "Quick login check milliseconds",
|
||||
"minimumQuickLoginWaitSeconds": "Minimum quick login wait"
|
||||
},
|
||||
"partial-import": {
|
||||
"partialImportHeaderText": "Partial import allows you to import users, clients, and other resources from a previously exported json file.",
|
||||
|
|
175
src/realm-settings/security-defences/BruteForceDetection.tsx
Normal file
175
src/realm-settings/security-defences/BruteForceDetection.tsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
NumberInput,
|
||||
Switch,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { Time } from "./Time";
|
||||
|
||||
type BruteForceDetectionProps = {
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const BruteForceDetection = ({
|
||||
save,
|
||||
reset,
|
||||
}: BruteForceDetectionProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isDirty },
|
||||
} = useFormContext();
|
||||
|
||||
const enable = useWatch({
|
||||
control,
|
||||
name: "bruteForceProtected",
|
||||
});
|
||||
|
||||
const permanentLockout = useWatch({
|
||||
control,
|
||||
name: "permanentLockout",
|
||||
});
|
||||
|
||||
return (
|
||||
<FormAccess role="manage-realm" isHorizontal onSubmit={handleSubmit(save)}>
|
||||
<FormGroup
|
||||
label={t("common:enabled")}
|
||||
fieldId="bruteForceProtected"
|
||||
hasNoPaddingTop
|
||||
>
|
||||
<Controller
|
||||
name="bruteForceProtected"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="bruteForceProtected"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{enable && (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("failureFactor")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="realm-settings-help:failureFactor"
|
||||
forLabel={t("failureFactor")}
|
||||
forID="failureFactor"
|
||||
/>
|
||||
}
|
||||
fieldId="failureFactor"
|
||||
>
|
||||
<Controller
|
||||
name="failureFactor"
|
||||
defaultValue={0}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ onChange, value }) => (
|
||||
<NumberInput
|
||||
type="text"
|
||||
id="failureFactor"
|
||||
value={value}
|
||||
onPlus={() => onChange(value + 1)}
|
||||
onMinus={() => onChange(value - 1)}
|
||||
onChange={(event) =>
|
||||
onChange(Number((event.target as HTMLInputElement).value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("permanentLockout")}
|
||||
fieldId="permanentLockout"
|
||||
hasNoPaddingTop
|
||||
>
|
||||
<Controller
|
||||
name="permanentLockout"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="permanentLockout"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{permanentLockout && (
|
||||
<>
|
||||
<Time name="waitIncrement" />
|
||||
<Time name="maxFailureWait" />
|
||||
<Time name="maxDeltaTime" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormGroup
|
||||
label={t("quickLoginCheckMilliSeconds")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="realm-settings-help:quickLoginCheckMilliSeconds"
|
||||
forLabel={t("quickLoginCheckMilliSeconds")}
|
||||
forID="quickLoginCheckMilliSeconds"
|
||||
/>
|
||||
}
|
||||
fieldId="quickLoginCheckMilliSeconds"
|
||||
>
|
||||
<Controller
|
||||
name="quickLoginCheckMilliSeconds"
|
||||
defaultValue={0}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<NumberInput
|
||||
type="text"
|
||||
id="quickLoginCheckMilliSeconds"
|
||||
value={value}
|
||||
onPlus={() => onChange(value + 1)}
|
||||
onMinus={() => onChange(value - 1)}
|
||||
onChange={(event) =>
|
||||
onChange(Number((event.target as HTMLInputElement).value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Time name="minimumQuickLoginWaitSeconds" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="brute-force-tab-save"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
75
src/realm-settings/security-defences/HeadersForm.tsx
Normal file
75
src/realm-settings/security-defences/HeadersForm.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { ActionGroup, Button } from "@patternfly/react-core";
|
||||
|
||||
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpLinkTextInput } from "./HelpLinkTextInput";
|
||||
|
||||
import "./security-defences.css";
|
||||
|
||||
type HeadersFormProps = {
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const HeadersForm = ({ save, reset }: HeadersFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
formState: { isDirty },
|
||||
handleSubmit,
|
||||
} = useFormContext();
|
||||
|
||||
return (
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="manage-realm"
|
||||
className="keycloak__security-defences__form"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.xFrameOptions"
|
||||
url="http://tools.ietf.org/html/rfc7034"
|
||||
/>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.contentSecurityPolicy"
|
||||
url="http://www.w3.org/TR/CSP/"
|
||||
/>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.contentSecurityPolicyReportOnly"
|
||||
url="http://www.w3.org/TR/CSP/"
|
||||
/>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.xContentTypeOptions"
|
||||
url="https://www.owasp.org/index.php/List_of_useful_HTTP_headers"
|
||||
/>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.xRobotsTag"
|
||||
url="https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag"
|
||||
/>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.xXSSProtection"
|
||||
url="https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xxxsp"
|
||||
/>
|
||||
<HelpLinkTextInput
|
||||
fieldName="browserSecurityHeaders.strictTransportSecurity"
|
||||
url="https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts"
|
||||
/>
|
||||
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="headers-form-tab-save"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
41
src/realm-settings/security-defences/HelpLinkTextInput.tsx
Normal file
41
src/realm-settings/security-defences/HelpLinkTextInput.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { FormattedLink } from "../../components/external-link/FormattedLink";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
type HelpLinkTextInputProps = {
|
||||
fieldName: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const HelpLinkTextInput = ({
|
||||
fieldName,
|
||||
url,
|
||||
}: HelpLinkTextInputProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const { register } = useFormContext();
|
||||
const name = fieldName.substr(fieldName.indexOf(".") + 1);
|
||||
return (
|
||||
<FormGroup
|
||||
label={t(name)}
|
||||
fieldId={name}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={
|
||||
<Trans i18nKey={`realm-settings-help:${name}`}>
|
||||
Default value prevents pages from being included
|
||||
<FormattedLink href={url} title={t("common:learnMore")} />
|
||||
</Trans>
|
||||
}
|
||||
forLabel={t(name)}
|
||||
forID={name}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput type="text" id={name} name={fieldName} ref={register} />
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
42
src/realm-settings/security-defences/SecurityDefences.tsx
Normal file
42
src/realm-settings/security-defences/SecurityDefences.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PageSection, Tab, Tabs, TabTitleText } from "@patternfly/react-core";
|
||||
|
||||
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
|
||||
import { HeadersForm } from "./HeadersForm";
|
||||
import { BruteForceDetection } from "./BruteForceDetection";
|
||||
|
||||
type SecurityDefencesProps = {
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const SecurityDefences = ({ save, reset }: SecurityDefencesProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const [activeTab, setActiveTab] = useState(10);
|
||||
return (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(_, key) => setActiveTab(key as number)}
|
||||
>
|
||||
<Tab
|
||||
id="headers"
|
||||
eventKey={10}
|
||||
title={<TabTitleText>{t("headers")}</TabTitleText>}
|
||||
>
|
||||
<PageSection variant="light">
|
||||
<HeadersForm save={save} reset={reset} />
|
||||
</PageSection>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="bruteForce"
|
||||
eventKey={20}
|
||||
title={<TabTitleText>{t("bruteForceDetection")}</TabTitleText>}
|
||||
>
|
||||
<PageSection variant="light">
|
||||
<BruteForceDetection save={save} reset={reset} />
|
||||
</PageSection>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
45
src/realm-settings/security-defences/Time.tsx
Normal file
45
src/realm-settings/security-defences/Time.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { TimeSelector } from "../../components/time-selector/TimeSelector";
|
||||
|
||||
export const Time = ({ name }: { name: string }) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const { control, errors } = useFormContext();
|
||||
return (
|
||||
<FormGroup
|
||||
label={t(name)}
|
||||
fieldId={name}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={`realm-settings-help:${name}`}
|
||||
forLabel={t(name)}
|
||||
forID={name}
|
||||
/>
|
||||
}
|
||||
validated={
|
||||
errors[name] ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<Controller
|
||||
name={name}
|
||||
defaultValue=""
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
validated={
|
||||
errors[name] ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
.keycloak__security-defences__form {
|
||||
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 12rem;
|
||||
}
|
Loading…
Reference in a new issue