mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 14:29:35 +00:00
feat(machine-identities): LDAP Auth Lockout
This commit is contained in:
@@ -192,6 +192,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
|
||||
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
|
||||
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity",
|
||||
[EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS]: "Clear LDAP Auth lockouts",
|
||||
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_LIST]: "List Secret Scanning Data Sources",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source",
|
||||
|
||||
@@ -54,6 +54,7 @@ export enum EventType {
|
||||
UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
|
||||
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
|
||||
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
|
||||
CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-ldap-auth-lockouts",
|
||||
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
|
||||
@@ -874,6 +874,13 @@ interface IntegrationSyncedEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ClearIdentityLdapAuthLockoutsEvent {
|
||||
type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -958,7 +965,8 @@ export type Event =
|
||||
| GetCertificateTemplateEstConfig
|
||||
| UpdateProjectWorkflowIntegrationConfig
|
||||
| GetProjectWorkflowIntegrationConfig
|
||||
| IntegrationSyncedEvent;
|
||||
| IntegrationSyncedEvent
|
||||
| ClearIdentityLdapAuthLockoutsEvent;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
AddIdentityTlsCertAuthDTO,
|
||||
AddIdentityTokenAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
ClearIdentityLdapAuthLockoutsDTO,
|
||||
ClearIdentityUniversalAuthLockoutsDTO,
|
||||
ClientSecretData,
|
||||
CreateIdentityDTO,
|
||||
@@ -1432,7 +1433,11 @@ export const useAddIdentityLdapAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
}) => {
|
||||
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
@@ -1448,7 +1453,11 @@ export const useAddIdentityLdapAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
}
|
||||
);
|
||||
return data.identityLdapAuth;
|
||||
@@ -1481,7 +1490,11 @@ export const useUpdateIdentityLdapAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
@@ -1497,7 +1510,11 @@ export const useUpdateIdentityLdapAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
}
|
||||
);
|
||||
return data.identityLdapAuth;
|
||||
@@ -1532,3 +1549,22 @@ export const useDeleteIdentityLdapAuth = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useClearIdentityLdapAuthLockouts = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<number, object, ClearIdentityLdapAuthLockoutsDTO>({
|
||||
mutationFn: async ({ identityId }) => {
|
||||
const {
|
||||
data: { deleted }
|
||||
} = await apiRequest.post<{ deleted: number }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}/clear-lockouts`
|
||||
);
|
||||
return deleted;
|
||||
},
|
||||
onSuccess: (_, { identityId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -603,6 +603,11 @@ export type AddIdentityLdapAuthDTO = {
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDuration: number;
|
||||
lockoutCounterReset: number;
|
||||
};
|
||||
|
||||
export type UpdateIdentityLdapAuthDTO = {
|
||||
@@ -625,6 +630,11 @@ export type UpdateIdentityLdapAuthDTO = {
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
|
||||
lockoutEnabled?: boolean;
|
||||
lockoutThreshold?: number;
|
||||
lockoutDuration?: number;
|
||||
lockoutCounterReset?: number;
|
||||
};
|
||||
|
||||
export type DeleteIdentityLdapAuthDTO = {
|
||||
@@ -650,6 +660,15 @@ export type IdentityLdapAuth = {
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDuration: number;
|
||||
lockoutCounterReset: number;
|
||||
};
|
||||
|
||||
export type ClearIdentityLdapAuthLockoutsDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type AddIdentityTokenAuthDTO = {
|
||||
|
||||
+103
-11
@@ -25,6 +25,7 @@ import {
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { durationToSeconds, getObjectFromSeconds } from "@app/helpers/datetime";
|
||||
import {
|
||||
MachineIdentityAuthMethod,
|
||||
useAddIdentityLdapAuth,
|
||||
@@ -35,6 +36,8 @@ import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LockoutTab } from "./lockout/LockoutTab";
|
||||
import { superRefineLockout } from "./lockout/super-refine";
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z
|
||||
@@ -74,9 +77,28 @@ const schema = z
|
||||
ipAddress: z.string().max(50)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.min(1),
|
||||
|
||||
lockoutEnabled: z.boolean().default(true),
|
||||
lockoutThreshold: z
|
||||
.string()
|
||||
.refine(
|
||||
(value) => Number(value) <= 30 && Number(value) >= 1,
|
||||
"Lockout threshold must be between 1 and 30"
|
||||
),
|
||||
lockoutDurationValue: z.string(),
|
||||
lockoutDurationUnit: z.enum(["s", "m", "h", "d"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
}),
|
||||
lockoutCounterResetValue: z.string(),
|
||||
lockoutCounterResetUnit: z.enum(["s", "m", "h"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
})
|
||||
})
|
||||
.required()
|
||||
.superRefine((data, ctx) => {
|
||||
superRefineLockout(data, ctx);
|
||||
|
||||
// Validation based on scope
|
||||
if (data.scope === "template") {
|
||||
if (!data.templateId) {
|
||||
@@ -178,12 +200,25 @@ export const IdentityLdapAuthForm = ({
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
lockoutEnabled: true,
|
||||
lockoutThreshold: "3",
|
||||
lockoutDurationValue: "5",
|
||||
lockoutDurationUnit: "m",
|
||||
lockoutCounterResetValue: "30",
|
||||
lockoutCounterResetUnit: "s"
|
||||
}
|
||||
});
|
||||
|
||||
const scope = watch("scope");
|
||||
|
||||
const lockoutEnabledWatch = watch("lockoutEnabled");
|
||||
const lockoutThresholdWatch = watch("lockoutThreshold");
|
||||
const lockoutDurationValueWatch = watch("lockoutDurationValue");
|
||||
const lockoutDurationUnitWatch = watch("lockoutDurationUnit");
|
||||
const lockoutCounterResetValueWatch = watch("lockoutCounterResetValue");
|
||||
const lockoutCounterResetUnitWatch = watch("lockoutCounterResetUnit");
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
@@ -210,6 +245,9 @@ export const IdentityLdapAuthForm = ({
|
||||
if (data) {
|
||||
const detectedScope = determineScope(data);
|
||||
|
||||
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDuration);
|
||||
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterReset);
|
||||
|
||||
reset({
|
||||
scope: detectedScope,
|
||||
templateId: data.templateId || "",
|
||||
@@ -229,7 +267,13 @@ export const IdentityLdapAuthForm = ({
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
};
|
||||
}
|
||||
)
|
||||
),
|
||||
lockoutEnabled: data.lockoutEnabled,
|
||||
lockoutThreshold: String(data.lockoutThreshold),
|
||||
lockoutDurationValue: String(lockoutDurationObj.value),
|
||||
lockoutDurationUnit: lockoutDurationObj.unit as "s" | "m" | "h" | "d",
|
||||
lockoutCounterResetValue: String(lockoutCounterResetObj.value),
|
||||
lockoutCounterResetUnit: lockoutCounterResetObj.unit as "s" | "m" | "h"
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -247,7 +291,13 @@ export const IdentityLdapAuthForm = ({
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
lockoutEnabled: true,
|
||||
lockoutThreshold: "3",
|
||||
lockoutDurationValue: "5",
|
||||
lockoutDurationUnit: "m",
|
||||
lockoutCounterResetValue: "30",
|
||||
lockoutCounterResetUnit: "s"
|
||||
});
|
||||
}, [data, reset]);
|
||||
|
||||
@@ -275,9 +325,21 @@ export const IdentityLdapAuthForm = ({
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationValue,
|
||||
lockoutDurationUnit,
|
||||
lockoutCounterResetValue,
|
||||
lockoutCounterResetUnit
|
||||
} = formData;
|
||||
|
||||
const lockoutDuration = durationToSeconds(Number(lockoutDurationValue), lockoutDurationUnit);
|
||||
const lockoutCounterReset = durationToSeconds(
|
||||
Number(lockoutCounterResetValue),
|
||||
lockoutCounterResetUnit
|
||||
);
|
||||
|
||||
const basePayload = {
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
@@ -287,7 +349,11 @@ export const IdentityLdapAuthForm = ({
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold: Number(lockoutThreshold),
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
};
|
||||
|
||||
// Add scope-specific fields
|
||||
@@ -327,7 +393,10 @@ export const IdentityLdapAuthForm = ({
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
const firstErrorField = Object.keys(fields)[0];
|
||||
let tab = IdentityFormTab.Advanced;
|
||||
|
||||
if (
|
||||
[
|
||||
"scope",
|
||||
"templateId",
|
||||
@@ -340,15 +409,29 @@ export const IdentityLdapAuthForm = ({
|
||||
"allowedFields",
|
||||
"accessTokenMaxTTL",
|
||||
"accessTokenNumUsesLimit"
|
||||
].includes(Object.keys(fields)[0])
|
||||
? IdentityFormTab.Configuration
|
||||
: IdentityFormTab.Advanced
|
||||
);
|
||||
].includes(firstErrorField)
|
||||
) {
|
||||
tab = IdentityFormTab.Configuration;
|
||||
} else if (
|
||||
[
|
||||
"lockoutEnabled",
|
||||
"lockoutThreshold",
|
||||
"lockoutDurationValue",
|
||||
"lockoutDurationUnit",
|
||||
"lockoutCounterResetValue",
|
||||
"lockoutCounterResetUnit"
|
||||
].includes(firstErrorField)
|
||||
) {
|
||||
tab = IdentityFormTab.Lockout;
|
||||
}
|
||||
|
||||
setTabValue(tab);
|
||||
})}
|
||||
>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||
<TabList>
|
||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={IdentityFormTab.Lockout}>Lockout</Tab>
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
@@ -691,6 +774,15 @@ export const IdentityLdapAuthForm = ({
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<LockoutTab
|
||||
control={control}
|
||||
lockoutEnabled={lockoutEnabledWatch}
|
||||
lockoutThreshold={lockoutThresholdWatch}
|
||||
lockoutDurationValue={lockoutDurationValueWatch}
|
||||
lockoutDurationUnit={lockoutDurationUnitWatch}
|
||||
lockoutCounterResetValue={lockoutCounterResetValueWatch}
|
||||
lockoutCounterResetUnit={lockoutCounterResetUnitWatch}
|
||||
/>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
+14
-243
@@ -11,9 +11,6 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -29,6 +26,8 @@ import {
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LockoutTab } from "./lockout/LockoutTab";
|
||||
import { superRefineLockout } from "./lockout/super-refine";
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z
|
||||
@@ -82,63 +81,7 @@ const schema = z
|
||||
})
|
||||
})
|
||||
.required()
|
||||
.superRefine((data, ctx) => {
|
||||
const {
|
||||
lockoutDurationValue,
|
||||
lockoutCounterResetValue,
|
||||
lockoutDurationUnit,
|
||||
lockoutCounterResetUnit,
|
||||
lockoutEnabled
|
||||
} = data;
|
||||
|
||||
if (!lockoutEnabled) return;
|
||||
|
||||
let isAnyParseError = false;
|
||||
|
||||
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
|
||||
if (Number.isNaN(parsedLockoutDuration)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout duration must be a number",
|
||||
path: ["lockoutDurationValue"]
|
||||
});
|
||||
isAnyParseError = true;
|
||||
}
|
||||
|
||||
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
|
||||
if (Number.isNaN(parsedLockoutCounterReset)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout counter reset must be a number",
|
||||
path: ["lockoutCounterResetValue"]
|
||||
});
|
||||
isAnyParseError = true;
|
||||
}
|
||||
|
||||
if (isAnyParseError) return;
|
||||
|
||||
const lockoutDurationInSeconds = durationToSeconds(parsedLockoutDuration, lockoutDurationUnit);
|
||||
const lockoutCounterResetInSeconds = durationToSeconds(
|
||||
parsedLockoutCounterReset,
|
||||
lockoutCounterResetUnit
|
||||
);
|
||||
|
||||
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout duration must be between 30 seconds and 1 day",
|
||||
path: ["lockoutDurationValue"]
|
||||
});
|
||||
}
|
||||
|
||||
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout counter reset must be between 5 seconds and 1 hour",
|
||||
path: ["lockoutCounterResetValue"]
|
||||
});
|
||||
}
|
||||
});
|
||||
.superRefine(superRefineLockout);
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
@@ -315,8 +258,8 @@ export const IdentityUniversalAuthForm = ({
|
||||
accessTokenPeriod: Number(accessTokenPeriod),
|
||||
lockoutEnabled,
|
||||
lockoutThreshold: Number(lockoutThreshold),
|
||||
lockoutDuration: Number(lockoutDuration),
|
||||
lockoutCounterReset: Number(lockoutCounterReset)
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
});
|
||||
}
|
||||
|
||||
@@ -435,187 +378,15 @@ export const IdentityUniversalAuthForm = ({
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Lockout}>
|
||||
<div className="mb-3 flex flex-col">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutEnabled"
|
||||
defaultValue
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValueWatch}${lockoutDurationUnitWatch} after ${lockoutThresholdWatch} consecutive login failures. If ${lockoutCounterResetValueWatch}${lockoutCounterResetUnitWatch} pass after the most recent failure, the lockout counter resets.`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="ml-0 mr-3 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
containerClassName="flex-row-reverse w-fit"
|
||||
id="lockout-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
Lockout {value ? "Enabled" : "Disabled"}
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutThreshold"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
label="Lockout Threshold"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The amount of times login must fail before locking the identity auth method"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout threshold..."
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutDurationValue"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
label="Lockout Duration"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="How long an identity auth method lockout lasts"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout duration..."
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutDurationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
value={field.value}
|
||||
className="min-w-32 pr-2"
|
||||
onValueChange={field.onChange}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value="s"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Seconds</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="m"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Minutes</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="h"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Hours</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="d"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Days</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutCounterResetValue"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
label="Lockout Counter Reset"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout counter reset..."
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutCounterResetUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
value={field.value}
|
||||
className="min-w-32 pr-2"
|
||||
onValueChange={field.onChange}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value="s"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Seconds</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="m"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Minutes</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="h"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Hours</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<LockoutTab
|
||||
control={control}
|
||||
lockoutEnabled={lockoutEnabledWatch}
|
||||
lockoutThreshold={lockoutThresholdWatch}
|
||||
lockoutDurationValue={lockoutDurationValueWatch}
|
||||
lockoutDurationUnit={lockoutDurationUnitWatch}
|
||||
lockoutCounterResetValue={lockoutCounterResetValueWatch}
|
||||
lockoutCounterResetUnit={lockoutCounterResetUnitWatch}
|
||||
/>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, Select, SelectItem, Switch, TabPanel } from "@app/components/v2";
|
||||
|
||||
import { IdentityFormTab } from "../types";
|
||||
|
||||
export const LockoutTab = ({
|
||||
control,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationValue,
|
||||
lockoutDurationUnit,
|
||||
lockoutCounterResetValue,
|
||||
lockoutCounterResetUnit
|
||||
}: {
|
||||
control: Control<any>;
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: string;
|
||||
lockoutDurationValue: string;
|
||||
lockoutDurationUnit: "s" | "m" | "h" | "d";
|
||||
lockoutCounterResetValue: string;
|
||||
lockoutCounterResetUnit: "s" | "m" | "h";
|
||||
}) => {
|
||||
return (
|
||||
<TabPanel value={IdentityFormTab.Lockout}>
|
||||
<div className="mb-3 flex flex-col">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutEnabled"
|
||||
defaultValue
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValue}${lockoutDurationUnit} after ${lockoutThreshold} consecutive login failures. If ${lockoutCounterResetValue}${lockoutCounterResetUnit} pass after the most recent failure, the lockout counter resets.`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="ml-0 mr-3 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
containerClassName="flex-row-reverse w-fit"
|
||||
id="lockout-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
Lockout {value ? "Enabled" : "Disabled"}
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutThreshold"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||
label="Lockout Threshold"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The amount of times login must fail before locking the identity auth method"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout threshold..."
|
||||
isDisabled={!lockoutEnabled}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutDurationValue"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||
label="Lockout Duration"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="How long an identity auth method lockout lasts"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout duration..."
|
||||
isDisabled={!lockoutEnabled}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutDurationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={`mb-0 ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={!lockoutEnabled}
|
||||
value={field.value}
|
||||
className="min-w-32 pr-2"
|
||||
onValueChange={field.onChange}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value="s"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Seconds</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="m"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Minutes</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="h"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Hours</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="d"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Days</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutCounterResetValue"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||
label="Lockout Counter Reset"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout counter reset..."
|
||||
isDisabled={!lockoutEnabled}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutCounterResetUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={`mb-0 ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={!lockoutEnabled}
|
||||
value={field.value}
|
||||
className="min-w-32 pr-2"
|
||||
onValueChange={field.onChange}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value="s"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Seconds</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="m"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Minutes</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="h"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Hours</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { durationToSeconds } from "@app/helpers/datetime";
|
||||
|
||||
export function superRefineLockout(
|
||||
data: {
|
||||
lockoutDurationValue: string;
|
||||
lockoutCounterResetValue: string;
|
||||
lockoutDurationUnit: "s" | "m" | "h" | "d";
|
||||
lockoutCounterResetUnit: "s" | "m" | "h";
|
||||
lockoutEnabled: boolean;
|
||||
},
|
||||
ctx: z.RefinementCtx
|
||||
) {
|
||||
const {
|
||||
lockoutDurationValue,
|
||||
lockoutCounterResetValue,
|
||||
lockoutDurationUnit,
|
||||
lockoutCounterResetUnit,
|
||||
lockoutEnabled
|
||||
} = data;
|
||||
|
||||
if (lockoutEnabled) {
|
||||
let isAnyParseError = false;
|
||||
|
||||
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
|
||||
if (Number.isNaN(parsedLockoutDuration)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout duration must be a number",
|
||||
path: ["lockoutDurationValue"]
|
||||
});
|
||||
isAnyParseError = true;
|
||||
}
|
||||
|
||||
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
|
||||
if (Number.isNaN(parsedLockoutCounterReset)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout counter reset must be a number",
|
||||
path: ["lockoutCounterResetValue"]
|
||||
});
|
||||
isAnyParseError = true;
|
||||
}
|
||||
|
||||
if (!isAnyParseError) {
|
||||
const lockoutDurationInSeconds = durationToSeconds(
|
||||
parsedLockoutDuration,
|
||||
lockoutDurationUnit
|
||||
);
|
||||
const lockoutCounterResetInSeconds = durationToSeconds(
|
||||
parsedLockoutCounterReset,
|
||||
lockoutCounterResetUnit
|
||||
);
|
||||
|
||||
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout duration must be between 30 seconds and 1 day",
|
||||
path: ["lockoutDurationValue"]
|
||||
});
|
||||
}
|
||||
|
||||
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout counter reset must be between 5 seconds and 1 hour",
|
||||
path: ["lockoutCounterResetValue"]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { useState } from "react";
|
||||
import { UseMutationResult } from "@tanstack/react-query";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||
|
||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||
|
||||
export const LockoutFields = ({
|
||||
clearLockoutsResult,
|
||||
lockedOut,
|
||||
identityId,
|
||||
data
|
||||
}: {
|
||||
clearLockoutsResult: UseMutationResult<number, object, { identityId: string }, unknown>;
|
||||
lockedOut: boolean;
|
||||
identityId: string;
|
||||
data: {
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDuration: number;
|
||||
lockoutCounterReset: number;
|
||||
};
|
||||
}) => {
|
||||
const { mutateAsync, isPending } = clearLockoutsResult;
|
||||
|
||||
const [lockedOutState, setLockedOutState] = useState(lockedOut);
|
||||
|
||||
async function clearLockouts() {
|
||||
try {
|
||||
const deleted = await mutateAsync({ identityId });
|
||||
createNotification({
|
||||
text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`,
|
||||
type: "success"
|
||||
});
|
||||
setLockedOutState(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to clear lockouts. Please try again.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
|
||||
<span className="text-bunker-300">Lockout Options</span>
|
||||
<OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed || !lockedOutState || isPending}
|
||||
size="xs"
|
||||
onClick={() => clearLockouts()}
|
||||
isLoading={isPending}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Clear All Lockouts
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityAuthFieldDisplay label="Lockout">
|
||||
{data.lockoutEnabled ? "Enabled" : "Disabled"}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Threshold">
|
||||
{data.lockoutThreshold}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Duration">
|
||||
{data.lockoutDuration} seconds
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Counter Reset">
|
||||
{data.lockoutCounterReset} seconds
|
||||
</IdentityAuthFieldDisplay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+11
-2
@@ -2,11 +2,12 @@ import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useGetIdentityLdapAuth } from "@app/hooks/api";
|
||||
import { useClearIdentityLdapAuthLockouts, useGetIdentityLdapAuth } from "@app/hooks/api";
|
||||
import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
|
||||
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
||||
|
||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||
import { LockoutFields } from "./IdentityAuthLockoutFields";
|
||||
import { ViewAuthMethodProps } from "./types";
|
||||
|
||||
export const ViewIdentityLdapAuthContent = ({
|
||||
@@ -14,9 +15,11 @@ export const ViewIdentityLdapAuthContent = ({
|
||||
handlePopUpToggle,
|
||||
handlePopUpOpen,
|
||||
onDelete,
|
||||
popUp
|
||||
popUp,
|
||||
lockedOut
|
||||
}: ViewAuthMethodProps) => {
|
||||
const { data, isPending } = useGetIdentityLdapAuth(identityId);
|
||||
const clearLockoutsResult = useClearIdentityLdapAuthLockouts();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
@@ -98,6 +101,12 @@ export const ViewIdentityLdapAuthContent = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<LockoutFields
|
||||
identityId={identityId}
|
||||
lockedOut={lockedOut}
|
||||
clearLockoutsResult={clearLockoutsResult}
|
||||
data={data}
|
||||
/>
|
||||
</ViewIdentityContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-54
@@ -1,11 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import {
|
||||
useClearIdentityUniversalAuthLockouts,
|
||||
@@ -15,6 +11,7 @@ import {
|
||||
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
|
||||
|
||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||
import { LockoutFields } from "./IdentityAuthLockoutFields";
|
||||
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
|
||||
import { ViewAuthMethodProps } from "./types";
|
||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
||||
@@ -30,32 +27,12 @@ export const ViewIdentityUniversalAuthContent = ({
|
||||
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
||||
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
||||
useGetIdentityUniversalAuthClientSecrets(identityId);
|
||||
const { mutateAsync: clearLockoutsFn, isPending: isClearLockoutsPending } =
|
||||
useClearIdentityUniversalAuthLockouts();
|
||||
|
||||
const [lockedOutState, setLockedOutState] = useState(lockedOut);
|
||||
const clearLockoutsResult = useClearIdentityUniversalAuthLockouts();
|
||||
|
||||
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
||||
initialState: "Copy Client ID to clipboard"
|
||||
});
|
||||
|
||||
async function clearLockouts() {
|
||||
try {
|
||||
const deleted = await clearLockoutsFn({ identityId });
|
||||
createNotification({
|
||||
text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`,
|
||||
type: "success"
|
||||
});
|
||||
setLockedOutState(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to clear lockouts. Please try again.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isPending || clientSecretsPending) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
@@ -112,34 +89,12 @@ export const ViewIdentityUniversalAuthContent = ({
|
||||
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
||||
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
|
||||
<span className="text-bunker-300">Lockout Options</span>
|
||||
<OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed || !lockedOutState || isClearLockoutsPending}
|
||||
size="xs"
|
||||
onClick={() => clearLockouts()}
|
||||
isLoading={isClearLockoutsPending}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Clear All Lockouts
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityAuthFieldDisplay label="Lockout">
|
||||
{data.lockoutEnabled ? "Enabled" : "Disabled"}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Threshold">
|
||||
{data.lockoutThreshold}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Duration">
|
||||
{data.lockoutDuration} seconds
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Counter Reset">
|
||||
{data.lockoutCounterReset} seconds
|
||||
</IdentityAuthFieldDisplay>
|
||||
<LockoutFields
|
||||
identityId={identityId}
|
||||
lockedOut={lockedOut}
|
||||
clearLockoutsResult={clearLockoutsResult}
|
||||
data={data}
|
||||
/>
|
||||
<div className="col-span-2 my-3">
|
||||
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
||||
<span className="text-bunker-300">Client ID</span>
|
||||
|
||||
Reference in New Issue
Block a user