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:
parent
0853e20ba1
commit
e58dfc7508
5 changed files with 170 additions and 148 deletions
|
@ -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);
|
||||
}
|
||||
},
|
||||
[selectedGroups]
|
||||
);
|
||||
|
||||
const setupForm = (user: UserRepresentation) => {
|
||||
reset();
|
||||
Object.entries(user).map((entry) => {
|
||||
setValue(entry[0], entry[1]);
|
||||
});
|
||||
const unLockUser = async () => {
|
||||
try {
|
||||
await adminClient.attackDetection.del({ id: user!.id! });
|
||||
addAlert(t("unlockSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("users:unlockError", error);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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 } };
|
||||
}
|
||||
};
|
||||
setTimeout(update, 100);
|
||||
}, []);
|
||||
return { user: undefined };
|
||||
},
|
||||
({ user, bruteForced }) => {
|
||||
setUser(user);
|
||||
setBruteForced(bruteForced);
|
||||
user && setupForm(user);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const setupForm = (user: UserRepresentation) => {
|
||||
userForm.reset(user);
|
||||
};
|
||||
|
||||
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,59 +91,63 @@ 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 && (
|
||||
<KeycloakTabs isBox>
|
||||
<Tab
|
||||
eventKey="settings"
|
||||
data-testid="user-details-tab"
|
||||
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||
>
|
||||
<PageSection variant="light">
|
||||
<UserForm
|
||||
onGroupsUpdate={updateGroups}
|
||||
form={userForm}
|
||||
save={save}
|
||||
editMode={true}
|
||||
/>
|
||||
</PageSection>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
data-testid="user-groups-tab"
|
||||
title={<TabTitleText>{t("groups")}</TabTitleText>}
|
||||
>
|
||||
<UserGroups />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="consents"
|
||||
data-testid="user-consents-tab"
|
||||
title={<TabTitleText>{t("users:consents")}</TabTitleText>}
|
||||
>
|
||||
<UserConsents />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="identity-provider-links"
|
||||
data-testid="identity-provider-links-tab"
|
||||
title={
|
||||
<TabTitleText>{t("users:identityProviderLinks")}</TabTitleText>
|
||||
}
|
||||
>
|
||||
<UserIdentityProviderLinks />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && (
|
||||
<PageSection variant="light">
|
||||
<UserForm
|
||||
onGroupsUpdate={updateGroups}
|
||||
form={userForm}
|
||||
save={save}
|
||||
editMode={false}
|
||||
/>
|
||||
</PageSection>
|
||||
)}
|
||||
<FormProvider {...userForm}>
|
||||
{id && user && (
|
||||
<KeycloakTabs isBox>
|
||||
<Tab
|
||||
eventKey="settings"
|
||||
data-testid="user-details-tab"
|
||||
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||
>
|
||||
<PageSection variant="light">
|
||||
{bruteForced && (
|
||||
<UserForm
|
||||
onGroupsUpdate={updateGroups}
|
||||
save={save}
|
||||
user={user}
|
||||
bruteForce={bruteForced}
|
||||
/>
|
||||
)}
|
||||
</PageSection>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
data-testid="user-groups-tab"
|
||||
title={<TabTitleText>{t("groups")}</TabTitleText>}
|
||||
>
|
||||
<UserGroups user={user} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="consents"
|
||||
data-testid="user-consents-tab"
|
||||
title={<TabTitleText>{t("users:consents")}</TabTitleText>}
|
||||
>
|
||||
<UserConsents />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="identity-provider-links"
|
||||
data-testid="identity-provider-links-tab"
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("users:identityProviderLinks")}
|
||||
</TabTitleText>
|
||||
}
|
||||
>
|
||||
<UserIdentityProviderLinks />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && (
|
||||
<PageSection variant="light">
|
||||
<UserForm onGroupsUpdate={updateGroups} save={save} />
|
||||
</PageSection>
|
||||
)}
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue