added unlock user when brute force detect on (#1093)

* added unlock user when brute force detect on

+ some light refactor

* fixed spelling

* fixed merge error
This commit is contained in:
Erik Jan de Wit 2021-09-02 15:44:31 +02:00 committed by GitHub
parent 0853e20ba1
commit e58dfc7508
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 170 additions and 148 deletions

View file

@ -13,31 +13,39 @@ import {
TextInput,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { Controller, UseFormMethods } from "react-hook-form";
import { useHistory, useParams } from "react-router-dom";
import { Controller, useFormContext } from "react-hook-form";
import { useHistory } from "react-router-dom";
import { FormAccess } from "../components/form-access/FormAccess";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { useRealm } from "../context/realm-context/RealmContext";
import { useFetch, useAdminClient } from "../context/auth/AdminClient";
import moment from "moment";
import { useAdminClient } from "../context/auth/AdminClient";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import { useAlerts } from "../components/alert/Alerts";
import { emailRegexPattern } from "../util";
import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
import moment from "moment";
export type BruteForced = {
isBruteForceProtected?: boolean;
isLocked?: boolean;
};
export type UserFormProps = {
form: UseFormMethods<UserRepresentation>;
user?: UserRepresentation;
bruteForce?: BruteForced;
save: (user: UserRepresentation) => void;
editMode: boolean;
timestamp?: number;
onGroupsUpdate: (groups: GroupRepresentation[]) => void;
};
export const UserForm = ({
form: { handleSubmit, register, errors, watch, control, setValue, reset },
user,
bruteForce: { isBruteForceProtected, isLocked } = {
isBruteForceProtected: false,
isLocked: false,
},
save,
editMode,
onGroupsUpdate,
}: UserFormProps) => {
const { t } = useTranslation("users");
@ -49,36 +57,24 @@ export const UserForm = ({
] = useState(false);
const history = useHistory();
const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>();
const { addAlert, addError } = useAlerts();
const { handleSubmit, register, errors, watch, control, reset } =
useFormContext();
const watchUsernameInput = watch("username");
const [user, setUser] = useState<UserRepresentation>();
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
[]
);
const { addAlert, addError } = useAlerts();
const [open, setOpen] = useState(false);
const [locked, setLocked] = useState(isLocked);
useFetch(
async () => {
if (editMode) return await adminClient.users.findOne({ id: id });
},
(user) => {
if (user) {
setupForm(user);
setUser(user);
const unLockUser = async () => {
try {
await adminClient.attackDetection.del({ id: user!.id! });
addAlert(t("unlockSuccess"), AlertVariant.success);
} catch (error) {
addError("users:unlockError", error);
}
},
[selectedGroups]
);
const setupForm = (user: UserRepresentation) => {
reset();
Object.entries(user).map((entry) => {
setValue(entry[0], entry[1]);
});
};
const requiredUserActionsOptions = [
@ -116,7 +112,7 @@ export const UserForm = ({
newGroups.forEach(async (group) => {
try {
await adminClient.users.addToGroup({
id,
id: user!.id!,
groupId: group.id!,
});
addAlert(t("users:addedGroupMembership"), AlertVariant.success);
@ -145,17 +141,17 @@ export const UserForm = ({
ok: "users:join",
}}
onConfirm={(groups) => {
editMode ? addGroups(groups) : addChips(groups);
user?.id ? addGroups(groups) : addChips(groups);
setOpen(false);
}}
onClose={() => setOpen(false)}
filterGroups={selectedGroups}
/>
)}
{editMode && user ? (
{user?.id ? (
<>
<FormGroup label={t("common:id")} fieldId="kc-id" isRequired>
<TextInput id={user.id} value={user.id} type="text" isReadOnly />
<TextInput id={user?.id} value={user?.id} type="text" isReadOnly />
</FormGroup>
<FormGroup label={t("createdAt")} fieldId="kc-created-at" isRequired>
<TextInput
@ -182,7 +178,7 @@ export const UserForm = ({
type="text"
id="kc-username"
name="username"
isReadOnly={editMode}
isReadOnly={!!user?.id}
/>
</FormGroup>
)}
@ -260,6 +256,32 @@ export const UserForm = ({
aria-label={t("lastName")}
/>
</FormGroup>
{isBruteForceProtected && (
<FormGroup
label={t("temporaryLocked")}
fieldId="temporaryLocked"
labelIcon={
<HelpItem
helpText="users-help:temporaryLocked"
forLabel={t("temporaryLocked")}
forID={t(`common:helpLabel`, { label: t("temporaryLocked") })}
/>
}
>
<Switch
data-testid="user-locked-switch"
id={"temporaryLocked"}
onChange={(value) => {
unLockUser();
setLocked(value);
}}
isChecked={locked}
isDisabled={!locked}
label={t("common:on")}
labelOff={t("common:off")}
/>
</FormGroup>
)}
<FormGroup
label={t("common:enabled")}
fieldId="kc-enabled"
@ -333,7 +355,7 @@ export const UserForm = ({
)}
/>
</FormGroup>
{!editMode && (
{!user?.id && (
<FormGroup
label={t("common:groups")}
fieldId="kc-groups"
@ -380,21 +402,21 @@ export const UserForm = ({
<ActionGroup>
<Button
data-testid={!editMode ? "create-user" : "save-user"}
isDisabled={!editMode && !watchUsernameInput}
data-testid={!user?.id ? "create-user" : "save-user"}
isDisabled={!user?.id && !watchUsernameInput}
variant="primary"
type="submit"
>
{editMode ? t("common:save") : t("common:create")}
{user?.id ? t("common:save") : t("common:create")}
</Button>
<Button
data-testid="cancel-create-user"
onClick={() =>
editMode ? setupForm(user!) : history.push(`/${realm}/users`)
user?.id ? reset(user) : history.push(`/${realm}/users`)
}
variant="link"
>
{editMode ? t("common:revert") : t("common:cancel")}
{user?.id ? t("common:revert") : t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>

View file

@ -12,7 +12,6 @@ import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/us
import _ from "lodash";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { GroupPath } from "../components/group/GroupPath";
@ -20,31 +19,21 @@ import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
import { useHelp } from "../components/help-enabler/HelpHeader";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useAdminClient } from "../context/auth/AdminClient";
import { emptyFormatter } from "../util";
export type UserFormProps = {
username?: string;
loader?: (
first?: number,
max?: number,
search?: string
) => Promise<UserRepresentation[]>;
addGroup?: (newGroup: GroupRepresentation) => void;
type UserGroupsProps = {
user: UserRepresentation;
};
export const UserGroups = () => {
export const UserGroups = ({ user }: UserGroupsProps) => {
const { t } = useTranslation("users");
const { addAlert, addError } = useAlerts();
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const [selectedGroup, setSelectedGroup] = useState<GroupRepresentation>();
const [list, setList] = useState(false);
const [listGroups, setListGroups] = useState(true);
const [search, setSearch] = useState("");
const [username, setUsername] = useState("");
const [isDirectMembership, setDirectMembership] = useState(true);
const [directMembershipList, setDirectMembershipList] = useState<
@ -55,7 +44,6 @@ export const UserGroups = () => {
const { enabled } = useHelp();
const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>();
const alphabetize = (groupsList: GroupRepresentation[]) => {
return _.sortBy(groupsList, (group) => group.path?.toUpperCase());
};
@ -66,22 +54,15 @@ export const UserGroups = () => {
max: max!,
};
const user = await adminClient.users.findOne({ id });
setUsername(user.username!);
const searchParam = search || "";
if (searchParam) {
params.search = searchParam;
setSearch(searchParam);
}
if (!searchParam && !listGroups && !list) {
return [];
}
const joinedUserGroups = await adminClient.users.listGroups({
...params,
id,
id: user.id!,
});
const allCreatedGroups = await adminClient.groups.find();
@ -177,14 +158,6 @@ export const UserGroups = () => {
return alphabetize(directMembership);
};
useFetch(
() => adminClient.users.listGroups({ id }),
(response) => {
setListGroups(!!(response && response.length > 0));
},
[]
);
useEffect(() => {
refresh();
}, [isDirectMembership]);
@ -201,14 +174,14 @@ export const UserGroups = () => {
}),
messageKey: t("leaveGroupConfirmDialog", {
groupname: selectedGroup?.name,
username: username,
username: user.username,
}),
continueButtonLabel: "leave",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.users.delFromGroup({
id,
id: user.id!,
groupId: selectedGroup!.id!,
});
refresh();
@ -248,10 +221,9 @@ export const UserGroups = () => {
newGroups.forEach(async (group) => {
try {
await adminClient.users.addToGroup({
id: id,
id: user.id!,
groupId: group.id!,
});
setList(true);
refresh();
addAlert(t("addedGroupMembership"), AlertVariant.success);
} catch (error) {
@ -267,10 +239,10 @@ export const UserGroups = () => {
<DeleteConfirm />
{open && (
<GroupPickerDialog
id={id}
id={user.id}
type="selectMany"
text={{
title: t("joinGroupsFor", { username }),
title: t("joinGroupsFor", { username: user.username }),
ok: "users:join",
}}
onClose={() => setOpen(false)}

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
AlertVariant,
PageSection,
@ -6,20 +6,21 @@ import {
TabTitleText,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
import { ViewHeader } from "../components/view-header/ViewHeader";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { UserForm } from "./UserForm";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { BruteForced, UserForm } from "./UserForm";
import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useHistory, useParams } from "react-router-dom";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { UserGroups } from "./UserGroups";
import { UserConsents } from "./UserConsents";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import { useRealm } from "../context/realm-context/RealmContext";
import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks";
import { toUser } from "./routes/User";
export const UsersTabs = () => {
const { t } = useTranslation("roles");
@ -30,18 +31,36 @@ export const UsersTabs = () => {
const adminClient = useAdminClient();
const userForm = useForm<UserRepresentation>({ mode: "onChange" });
const { id } = useParams<{ id: string }>();
const [user, setUser] = useState("");
const [user, setUser] = useState<UserRepresentation>();
const [bruteForced, setBruteForced] = useState<BruteForced>();
const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]);
useEffect(() => {
const update = async () => {
useFetch(
async () => {
if (id) {
const fetchedUser = await adminClient.users.findOne({ id });
setUser(fetchedUser.username!);
const user = await adminClient.users.findOne({ id });
const isBruteForceProtected = (
await adminClient.realms.findOne({ realm })
).bruteForceProtected;
const isLocked: boolean =
isBruteForceProtected &&
(await adminClient.attackDetection.findOne({ id: user.id! }))
?.disabled;
return { user, bruteForced: { isBruteForceProtected, isLocked } };
}
return { user: undefined };
},
({ user, bruteForced }) => {
setUser(user);
setBruteForced(bruteForced);
user && setupForm(user);
},
[]
);
const setupForm = (user: UserRepresentation) => {
userForm.reset(user);
};
setTimeout(update, 100);
}, []);
const updateGroups = (groups: GroupRepresentation[]) => {
setAddedGroups(groups);
@ -63,7 +82,7 @@ export const UsersTabs = () => {
});
addAlert(t("users:userCreated"), AlertVariant.success);
history.push(`/${realm}/users/${createdUser.id}/settings`);
history.push(toUser({ id: createdUser.id, realm, tab: "settings" }));
}
} catch (error) {
addError("users:userCreateError", error);
@ -72,9 +91,13 @@ export const UsersTabs = () => {
return (
<>
<ViewHeader titleKey={user! || t("users:createUser")} divider={!id} />
<ViewHeader
titleKey={user?.username || t("users:createUser")}
divider={!id}
/>
<PageSection variant="light" className="pf-u-p-0">
{id && (
<FormProvider {...userForm}>
{id && user && (
<KeycloakTabs isBox>
<Tab
eventKey="settings"
@ -82,12 +105,14 @@ export const UsersTabs = () => {
title={<TabTitleText>{t("details")}</TabTitleText>}
>
<PageSection variant="light">
{bruteForced && (
<UserForm
onGroupsUpdate={updateGroups}
form={userForm}
save={save}
editMode={true}
user={user}
bruteForce={bruteForced}
/>
)}
</PageSection>
</Tab>
<Tab
@ -95,7 +120,7 @@ export const UsersTabs = () => {
data-testid="user-groups-tab"
title={<TabTitleText>{t("groups")}</TabTitleText>}
>
<UserGroups />
<UserGroups user={user} />
</Tab>
<Tab
eventKey="consents"
@ -108,7 +133,9 @@ export const UsersTabs = () => {
eventKey="identity-provider-links"
data-testid="identity-provider-links-tab"
title={
<TabTitleText>{t("users:identityProviderLinks")}</TabTitleText>
<TabTitleText>
{t("users:identityProviderLinks")}
</TabTitleText>
}
>
<UserIdentityProviderLinks />
@ -117,14 +144,10 @@ export const UsersTabs = () => {
)}
{!id && (
<PageSection variant="light">
<UserForm
onGroupsUpdate={updateGroups}
form={userForm}
save={save}
editMode={false}
/>
<UserForm onGroupsUpdate={updateGroups} save={save} />
</PageSection>
)}
</FormProvider>
</PageSection>
</>
);

View file

@ -1,5 +1,7 @@
export default {
"users-help": {
temporaryLocked:
"The user may be locked due to multiple failed attempts to log in.",
disabled: "A disabled user cannot log in.",
emailVerified: "Has the user's email been verified?",
requiredUserActions:

View file

@ -38,6 +38,9 @@ export default {
firstName: "First name",
status: "Status",
disabled: "Disabled",
temporaryLocked: "Temporarily locked",
unlockSuccess: "User successfully unlocked",
unlockError: "Could not unlock user due to {{error}}",
emailInvalid: "You must enter a valid email.",
temporaryDisabled: "Temporarily disabled",
notVerified: "Not verified",