diff --git a/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts b/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts index 535e6de7c..6cc851368 100644 --- a/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts +++ b/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts @@ -6,8 +6,11 @@ export async function up(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.IdentityLdapAuth)) { const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutEnabled"); const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutThreshold"); - const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDuration"); - const hasLockoutCounterReset = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutCounterReset"); + const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDurationSeconds"); + const hasLockoutCounterReset = await knex.schema.hasColumn( + TableName.IdentityLdapAuth, + "lockoutCounterResetSeconds" + ); await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { if (!hasLockoutEnabled) { @@ -17,10 +20,10 @@ export async function up(knex: Knex): Promise { t.integer("lockoutThreshold").notNullable().defaultTo(3); } if (!hasLockoutDuration) { - t.integer("lockoutDuration").notNullable().defaultTo(300); // 5 minutes (in seconds) + t.integer("lockoutDurationSeconds").notNullable().defaultTo(300); // 5 minutes } if (!hasLockoutCounterReset) { - t.integer("lockoutCounterReset").notNullable().defaultTo(30); // 30 seconds + t.integer("lockoutCounterResetSeconds").notNullable().defaultTo(30); // 30 seconds } }); } @@ -30,8 +33,11 @@ export async function down(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.IdentityLdapAuth)) { const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutEnabled"); const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutThreshold"); - const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDuration"); - const hasLockoutCounterReset = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutCounterReset"); + const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDurationSeconds"); + const hasLockoutCounterReset = await knex.schema.hasColumn( + TableName.IdentityLdapAuth, + "lockoutCounterResetSeconds" + ); await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { if (hasLockoutEnabled) { @@ -41,10 +47,10 @@ export async function down(knex: Knex): Promise { t.dropColumn("lockoutThreshold"); } if (hasLockoutDuration) { - t.dropColumn("lockoutDuration"); + t.dropColumn("lockoutDurationSeconds"); } if (hasLockoutCounterReset) { - t.dropColumn("lockoutCounterReset"); + t.dropColumn("lockoutCounterResetSeconds"); } }); } diff --git a/backend/src/db/schemas/identity-ldap-auths.ts b/backend/src/db/schemas/identity-ldap-auths.ts index 3a89fdd51..87c7f1608 100644 --- a/backend/src/db/schemas/identity-ldap-auths.ts +++ b/backend/src/db/schemas/identity-ldap-auths.ts @@ -29,8 +29,8 @@ export const IdentityLdapAuthsSchema = z.object({ templateId: z.string().uuid().nullable().optional(), lockoutEnabled: z.boolean().default(true), lockoutThreshold: z.number().default(3), - lockoutDuration: z.number().default(300), - lockoutCounterReset: z.number().default(30) + lockoutDurationSeconds: z.number().default(300), + lockoutCounterResetSeconds: z.number().default(30) }); export type TIdentityLdapAuths = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index ae32bdab5..82d7ff80c 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1372,8 +1372,8 @@ interface AddIdentityLdapAuthEvent { templateId?: string | null; lockoutEnabled: boolean; lockoutThreshold: number; - lockoutDuration: number; - lockoutCounterReset: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; }; } @@ -1390,8 +1390,8 @@ interface UpdateIdentityLdapAuthEvent { templateId?: string | null; lockoutEnabled?: boolean; lockoutThreshold?: number; - lockoutDuration?: number; - lockoutCounterReset?: number; + lockoutDurationSeconds?: number; + lockoutCounterResetSeconds?: number; }; } diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ae7f9a58d..7edd421e6 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -245,8 +245,9 @@ export const LDAP_AUTH = { accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", lockoutEnabled: "Whether the lockout feature is enabled.", lockoutThreshold: "The amount of times login must fail before locking the identity auth method.", - lockoutDuration: "How long an identity auth method lockout lasts.", - lockoutCounterReset: "How long to wait from the most recent failed login until resetting the lockout counter." + lockoutDurationSeconds: "How long an identity auth method lockout lasts.", + lockoutCounterResetSeconds: + "How long to wait from the most recent failed login until resetting the lockout counter." }, UPDATE: { identityId: "The ID of the identity to update the configuration for.", @@ -264,8 +265,9 @@ export const LDAP_AUTH = { templateId: "The ID of the identity auth template to update the configuration to.", lockoutEnabled: "Whether the lockout feature is enabled.", lockoutThreshold: "The amount of times login must fail before locking the identity auth method.", - lockoutDuration: "How long an identity auth method lockout lasts.", - lockoutCounterReset: "How long to wait from the most recent failed login until resetting the lockout counter." + lockoutDurationSeconds: "How long an identity auth method lockout lasts.", + lockoutCounterResetSeconds: + "How long to wait from the most recent failed login until resetting the lockout counter." }, RETRIEVE: { identityId: "The ID of the identity to retrieve the configuration for." diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 5931204c1..832f428a8 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -266,8 +266,18 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit), lockoutEnabled: z.boolean().default(true).describe(LDAP_AUTH.ATTACH.lockoutEnabled), lockoutThreshold: z.number().min(1).max(30).default(3).describe(LDAP_AUTH.ATTACH.lockoutThreshold), - lockoutDuration: z.number().min(30).max(86400).default(300).describe(LDAP_AUTH.ATTACH.lockoutDuration), - lockoutCounterReset: z.number().min(5).max(3600).default(30).describe(LDAP_AUTH.ATTACH.lockoutCounterReset) + lockoutDurationSeconds: z + .number() + .min(30) + .max(86400) + .default(300) + .describe(LDAP_AUTH.ATTACH.lockoutDurationSeconds), + lockoutCounterResetSeconds: z + .number() + .min(5) + .max(3600) + .default(30) + .describe(LDAP_AUTH.ATTACH.lockoutCounterResetSeconds) }) .refine( (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, @@ -320,8 +330,18 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit), lockoutEnabled: z.boolean().default(true).describe(LDAP_AUTH.ATTACH.lockoutEnabled), lockoutThreshold: z.number().min(1).max(30).default(3).describe(LDAP_AUTH.ATTACH.lockoutThreshold), - lockoutDuration: z.number().min(30).max(86400).default(300).describe(LDAP_AUTH.ATTACH.lockoutDuration), - lockoutCounterReset: z.number().min(5).max(3600).default(30).describe(LDAP_AUTH.ATTACH.lockoutCounterReset) + lockoutDurationSeconds: z + .number() + .min(30) + .max(86400) + .default(300) + .describe(LDAP_AUTH.ATTACH.lockoutDurationSeconds), + lockoutCounterResetSeconds: z + .number() + .min(5) + .max(3600) + .default(30) + .describe(LDAP_AUTH.ATTACH.lockoutCounterResetSeconds) }) .refine( (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, @@ -364,8 +384,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) templateId: identityLdapAuth.templateId, lockoutEnabled: identityLdapAuth.lockoutEnabled, lockoutThreshold: identityLdapAuth.lockoutThreshold, - lockoutDuration: identityLdapAuth.lockoutDuration, - lockoutCounterReset: identityLdapAuth.lockoutCounterReset + lockoutDurationSeconds: identityLdapAuth.lockoutDurationSeconds, + lockoutCounterResetSeconds: identityLdapAuth.lockoutCounterResetSeconds } } }); @@ -432,8 +452,18 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) .describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL), lockoutEnabled: z.boolean().optional().describe(LDAP_AUTH.UPDATE.lockoutEnabled), lockoutThreshold: z.number().min(1).max(30).optional().describe(LDAP_AUTH.UPDATE.lockoutThreshold), - lockoutDuration: z.number().min(30).max(86400).optional().describe(LDAP_AUTH.UPDATE.lockoutDuration), - lockoutCounterReset: z.number().min(5).max(3600).optional().describe(LDAP_AUTH.UPDATE.lockoutCounterReset) + lockoutDurationSeconds: z + .number() + .min(30) + .max(86400) + .optional() + .describe(LDAP_AUTH.UPDATE.lockoutDurationSeconds), + lockoutCounterResetSeconds: z + .number() + .min(5) + .max(3600) + .optional() + .describe(LDAP_AUTH.UPDATE.lockoutCounterResetSeconds) }) .refine( (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), @@ -475,8 +505,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) templateId: identityLdapAuth.templateId, lockoutEnabled: identityLdapAuth.lockoutEnabled, lockoutThreshold: identityLdapAuth.lockoutThreshold, - lockoutDuration: identityLdapAuth.lockoutDuration, - lockoutCounterReset: identityLdapAuth.lockoutCounterReset + lockoutDurationSeconds: identityLdapAuth.lockoutDurationSeconds, + lockoutCounterResetSeconds: identityLdapAuth.lockoutCounterResetSeconds } } }); diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 9445b42e8..a8576dc0e 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -223,8 +223,8 @@ export const identityLdapAuthServiceFactory = ({ allowedFields, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }: TAttachLdapAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); @@ -360,8 +360,8 @@ export const identityLdapAuthServiceFactory = ({ templateId, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }, tx ); @@ -390,8 +390,8 @@ export const identityLdapAuthServiceFactory = ({ actorOrgId, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }: TUpdateLdapAuthDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -542,8 +542,8 @@ export const identityLdapAuthServiceFactory = ({ : undefined, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }); return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId }; @@ -687,7 +687,7 @@ export const identityLdapAuthServiceFactory = ({ await keyStore.setItemWithExpiry( LOCKOUT_KEY, - lockout.lockedOut ? identityLdapAuth.lockoutDuration : identityLdapAuth.lockoutCounterReset, + lockout.lockedOut ? identityLdapAuth.lockoutDurationSeconds : identityLdapAuth.lockoutCounterResetSeconds, JSON.stringify(lockout) ); } diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts index 543df1b2a..d6a4aba49 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -29,8 +29,8 @@ export type TAttachLdapAuthDTO = { isActorSuperAdmin?: boolean; lockoutEnabled: boolean; lockoutThreshold: number; - lockoutDuration: number; - lockoutCounterReset: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; } & Omit; export type TUpdateLdapAuthDTO = { @@ -49,8 +49,8 @@ export type TUpdateLdapAuthDTO = { accessTokenTrustedIps?: { ipAddress: string }[]; lockoutEnabled?: boolean; lockoutThreshold?: number; - lockoutDuration?: number; - lockoutCounterReset?: number; + lockoutDurationSeconds?: number; + lockoutCounterResetSeconds?: number; } & Omit; export type TGetLdapAuthDTO = { diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 6f6fa4fbd..4ada1fb9b 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -1436,8 +1436,8 @@ export const useAddIdentityLdapAuth = () => { accessTokenTrustedIps, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }) => { const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, @@ -1456,8 +1456,8 @@ export const useAddIdentityLdapAuth = () => { accessTokenTrustedIps, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds } ); return data.identityLdapAuth; @@ -1493,8 +1493,8 @@ export const useUpdateIdentityLdapAuth = () => { accessTokenTrustedIps, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }) => { const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, @@ -1513,8 +1513,8 @@ export const useUpdateIdentityLdapAuth = () => { accessTokenTrustedIps, lockoutEnabled, lockoutThreshold, - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds } ); return data.identityLdapAuth; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 07430770c..a2fa17acc 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -606,8 +606,8 @@ export type AddIdentityLdapAuthDTO = { lockoutEnabled: boolean; lockoutThreshold: number; - lockoutDuration: number; - lockoutCounterReset: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; }; export type UpdateIdentityLdapAuthDTO = { @@ -633,8 +633,8 @@ export type UpdateIdentityLdapAuthDTO = { lockoutEnabled?: boolean; lockoutThreshold?: number; - lockoutDuration?: number; - lockoutCounterReset?: number; + lockoutDurationSeconds?: number; + lockoutCounterResetSeconds?: number; }; export type DeleteIdentityLdapAuthDTO = { @@ -663,8 +663,8 @@ export type IdentityLdapAuth = { lockoutEnabled: boolean; lockoutThreshold: number; - lockoutDuration: number; - lockoutCounterReset: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; }; export type ClearIdentityLdapAuthLockoutsDTO = { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx index 1930f34c9..744942219 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx @@ -245,8 +245,8 @@ export const IdentityLdapAuthForm = ({ if (data) { const detectedScope = determineScope(data); - const lockoutDurationObj = getObjectFromSeconds(data.lockoutDuration); - const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterReset); + const lockoutDurationObj = getObjectFromSeconds(data.lockoutDurationSeconds); + const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterResetSeconds); reset({ scope: detectedScope, @@ -334,8 +334,11 @@ export const IdentityLdapAuthForm = ({ lockoutCounterResetUnit } = formData; - const lockoutDuration = durationToSeconds(Number(lockoutDurationValue), lockoutDurationUnit); - const lockoutCounterReset = durationToSeconds( + const lockoutDurationSeconds = durationToSeconds( + Number(lockoutDurationValue), + lockoutDurationUnit + ); + const lockoutCounterResetSeconds = durationToSeconds( Number(lockoutCounterResetValue), lockoutCounterResetUnit ); @@ -352,8 +355,8 @@ export const IdentityLdapAuthForm = ({ accessTokenTrustedIps, lockoutEnabled, lockoutThreshold: Number(lockoutThreshold), - lockoutDuration, - lockoutCounterReset + lockoutDurationSeconds, + lockoutCounterResetSeconds }; // Add scope-specific fields diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx index b2f39224e..aa55f418e 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { UseMutationResult } from "@tanstack/react-query"; +import ms from "ms"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -20,8 +21,8 @@ export const LockoutFields = ({ data: { lockoutEnabled: boolean; lockoutThreshold: number; - lockoutDuration: number; - lockoutCounterReset: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; }; }) => { const { mutateAsync, isPending } = clearLockoutsResult; @@ -70,10 +71,10 @@ export const LockoutFields = ({ {data.lockoutThreshold} - {data.lockoutDuration} seconds + {ms(data.lockoutDurationSeconds * 1000, { long: true })} - {data.lockoutCounterReset} seconds + {ms(data.lockoutCounterResetSeconds * 1000, { long: true })} ); diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx index 70487db99..7ef8790c2 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx @@ -1,6 +1,5 @@ import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import ms from "ms"; import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2"; import { useTimedReset } from "@app/hooks";