Merge branch 'ENG-3506' into ENG-3506-LDAP

This commit is contained in:
x032205
2025-08-26 20:45:28 -04:00
12 changed files with 87 additions and 65 deletions
@@ -6,8 +6,11 @@ export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) { if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled"); const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold"); const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDuration"); const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDurationSeconds");
const hasLockoutCounterReset = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutCounterReset"); const hasLockoutCounterReset = await knex.schema.hasColumn(
TableName.IdentityUniversalAuth,
"lockoutCounterResetSeconds"
);
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => { await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
if (!hasLockoutEnabled) { if (!hasLockoutEnabled) {
@@ -17,10 +20,10 @@ export async function up(knex: Knex): Promise<void> {
t.integer("lockoutThreshold").notNullable().defaultTo(3); t.integer("lockoutThreshold").notNullable().defaultTo(3);
} }
if (!hasLockoutDuration) { if (!hasLockoutDuration) {
t.integer("lockoutDuration").notNullable().defaultTo(300); // 5 minutes (in seconds) t.integer("lockoutDurationSeconds").notNullable().defaultTo(300); // 5 minutes
} }
if (!hasLockoutCounterReset) { 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<void> {
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) { if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled"); const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold"); const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDuration"); const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDurationSeconds");
const hasLockoutCounterReset = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutCounterReset"); const hasLockoutCounterReset = await knex.schema.hasColumn(
TableName.IdentityUniversalAuth,
"lockoutCounterResetSeconds"
);
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => { await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
if (hasLockoutEnabled) { if (hasLockoutEnabled) {
@@ -41,10 +47,10 @@ export async function down(knex: Knex): Promise<void> {
t.dropColumn("lockoutThreshold"); t.dropColumn("lockoutThreshold");
} }
if (hasLockoutDuration) { if (hasLockoutDuration) {
t.dropColumn("lockoutDuration"); t.dropColumn("lockoutDurationSeconds");
} }
if (hasLockoutCounterReset) { if (hasLockoutCounterReset) {
t.dropColumn("lockoutCounterReset"); t.dropColumn("lockoutCounterResetSeconds");
} }
}); });
} }
@@ -21,8 +21,8 @@ export const IdentityUniversalAuthsSchema = z.object({
accessTokenPeriod: z.coerce.number().default(0), accessTokenPeriod: z.coerce.number().default(0),
lockoutEnabled: z.boolean().default(true), lockoutEnabled: z.boolean().default(true),
lockoutThreshold: z.number().default(3), lockoutThreshold: z.number().default(3),
lockoutDuration: z.number().default(300), lockoutDurationSeconds: z.number().default(300),
lockoutCounterReset: z.number().default(30) lockoutCounterResetSeconds: z.number().default(30)
}); });
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>; export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
@@ -870,8 +870,8 @@ interface AddIdentityUniversalAuthEvent {
accessTokenTrustedIps: Array<TIdentityTrustedIp>; accessTokenTrustedIps: Array<TIdentityTrustedIp>;
lockoutEnabled: boolean; lockoutEnabled: boolean;
lockoutThreshold: number; lockoutThreshold: number;
lockoutDuration: number; lockoutDurationSeconds: number;
lockoutCounterReset: number; lockoutCounterResetSeconds: number;
}; };
} }
@@ -886,8 +886,8 @@ interface UpdateIdentityUniversalAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>; accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
lockoutEnabled?: boolean; lockoutEnabled?: boolean;
lockoutThreshold?: number; lockoutThreshold?: number;
lockoutDuration?: number; lockoutDurationSeconds?: number;
lockoutCounterReset?: number; lockoutCounterResetSeconds?: number;
}; };
} }
+6 -4
View File
@@ -169,8 +169,9 @@ export const UNIVERSAL_AUTH = {
"The period for an access token in seconds. This value will be referenced at renewal time. Default value is 0.", "The period for an access token in seconds. This value will be referenced at renewal time. Default value is 0.",
lockoutEnabled: "Whether the lockout feature is enabled.", lockoutEnabled: "Whether the lockout feature is enabled.",
lockoutThreshold: "The amount of times login must fail before locking the identity auth method.", lockoutThreshold: "The amount of times login must fail before locking the identity auth method.",
lockoutDuration: "How long an identity auth method lockout lasts.", lockoutDurationSeconds: "How long an identity auth method lockout lasts.",
lockoutCounterReset: "How long to wait from the most recent failed login until resetting the lockout counter." lockoutCounterResetSeconds:
"How long to wait from the most recent failed login until resetting the lockout counter."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the identity to retrieve the auth method for."
@@ -188,8 +189,9 @@ export const UNIVERSAL_AUTH = {
accessTokenPeriod: "The new period for an access token in seconds.", accessTokenPeriod: "The new period for an access token in seconds.",
lockoutEnabled: "Whether the lockout feature is enabled.", lockoutEnabled: "Whether the lockout feature is enabled.",
lockoutThreshold: "The amount of times login must fail before locking the identity auth method.", lockoutThreshold: "The amount of times login must fail before locking the identity auth method.",
lockoutDuration: "How long an identity auth method lockout lasts.", lockoutDurationSeconds: "How long an identity auth method lockout lasts.",
lockoutCounterReset: "How long to wait from the most recent failed login until resetting the lockout counter." lockoutCounterResetSeconds:
"How long to wait from the most recent failed login until resetting the lockout counter."
}, },
CREATE_CLIENT_SECRET: { CREATE_CLIENT_SECRET: {
identityId: "The ID of the identity to create a client secret for.", identityId: "The ID of the identity to create a client secret for.",
@@ -140,13 +140,18 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
accessTokenPeriod: z.number().int().min(0).default(0).describe(UNIVERSAL_AUTH.ATTACH.accessTokenPeriod), accessTokenPeriod: z.number().int().min(0).default(0).describe(UNIVERSAL_AUTH.ATTACH.accessTokenPeriod),
lockoutEnabled: z.boolean().default(true).describe(UNIVERSAL_AUTH.ATTACH.lockoutEnabled), lockoutEnabled: z.boolean().default(true).describe(UNIVERSAL_AUTH.ATTACH.lockoutEnabled),
lockoutThreshold: z.number().min(1).max(30).default(3).describe(UNIVERSAL_AUTH.ATTACH.lockoutThreshold), lockoutThreshold: z.number().min(1).max(30).default(3).describe(UNIVERSAL_AUTH.ATTACH.lockoutThreshold),
lockoutDuration: z.number().min(30).max(86400).default(300).describe(UNIVERSAL_AUTH.ATTACH.lockoutDuration), lockoutDurationSeconds: z
lockoutCounterReset: z .number()
.min(30)
.max(86400)
.default(300)
.describe(UNIVERSAL_AUTH.ATTACH.lockoutDurationSeconds),
lockoutCounterResetSeconds: z
.number() .number()
.min(5) .min(5)
.max(3600) .max(3600)
.default(30) .default(30)
.describe(UNIVERSAL_AUTH.ATTACH.lockoutCounterReset) .describe(UNIVERSAL_AUTH.ATTACH.lockoutCounterResetSeconds)
}) })
.refine( .refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL, (val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
@@ -183,8 +188,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit,
lockoutEnabled: identityUniversalAuth.lockoutEnabled, lockoutEnabled: identityUniversalAuth.lockoutEnabled,
lockoutThreshold: identityUniversalAuth.lockoutThreshold, lockoutThreshold: identityUniversalAuth.lockoutThreshold,
lockoutDuration: identityUniversalAuth.lockoutDuration, lockoutDurationSeconds: identityUniversalAuth.lockoutDurationSeconds,
lockoutCounterReset: identityUniversalAuth.lockoutCounterReset lockoutCounterResetSeconds: identityUniversalAuth.lockoutCounterResetSeconds
} }
} }
}); });
@@ -259,13 +264,18 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenPeriod), .describe(UNIVERSAL_AUTH.UPDATE.accessTokenPeriod),
lockoutEnabled: z.boolean().optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutEnabled), lockoutEnabled: z.boolean().optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutEnabled),
lockoutThreshold: z.number().min(1).max(30).optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutThreshold), lockoutThreshold: z.number().min(1).max(30).optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutThreshold),
lockoutDuration: z.number().min(30).max(86400).optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutDuration), lockoutDurationSeconds: z
lockoutCounterReset: z .number()
.min(30)
.max(86400)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.lockoutDurationSeconds),
lockoutCounterResetSeconds: z
.number() .number()
.min(5) .min(5)
.max(3600) .max(3600)
.optional() .optional()
.describe(UNIVERSAL_AUTH.UPDATE.lockoutCounterReset) .describe(UNIVERSAL_AUTH.UPDATE.lockoutCounterResetSeconds)
}) })
.refine( .refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
@@ -301,8 +311,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit,
lockoutEnabled: identityUniversalAuth.lockoutEnabled, lockoutEnabled: identityUniversalAuth.lockoutEnabled,
lockoutThreshold: identityUniversalAuth.lockoutThreshold, lockoutThreshold: identityUniversalAuth.lockoutThreshold,
lockoutDuration: identityUniversalAuth.lockoutDuration, lockoutDurationSeconds: identityUniversalAuth.lockoutDurationSeconds,
lockoutCounterReset: identityUniversalAuth.lockoutCounterReset lockoutCounterResetSeconds: identityUniversalAuth.lockoutCounterResetSeconds
} }
} }
}); });
@@ -131,7 +131,7 @@ export const identityUaServiceFactory = ({
await keyStore.setItemWithExpiry( await keyStore.setItemWithExpiry(
LOCKOUT_KEY, LOCKOUT_KEY,
lockout.lockedOut ? identityUa.lockoutDuration : identityUa.lockoutCounterReset, lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
JSON.stringify(lockout) JSON.stringify(lockout)
); );
} }
@@ -251,8 +251,8 @@ export const identityUaServiceFactory = ({
accessTokenPeriod, accessTokenPeriod,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}: TAttachUaDTO) => { }: TAttachUaDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
@@ -325,8 +325,8 @@ export const identityUaServiceFactory = ({
accessTokenPeriod, accessTokenPeriod,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}, },
tx tx
); );
@@ -349,8 +349,8 @@ export const identityUaServiceFactory = ({
actorOrgId, actorOrgId,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}: TUpdateUaDTO) => { }: TUpdateUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -429,8 +429,8 @@ export const identityUaServiceFactory = ({
: undefined, : undefined,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}); });
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
}; };
@@ -11,8 +11,8 @@ export type TAttachUaDTO = {
isActorSuperAdmin?: boolean; isActorSuperAdmin?: boolean;
lockoutEnabled: boolean; lockoutEnabled: boolean;
lockoutThreshold: number; lockoutThreshold: number;
lockoutDuration: number; lockoutDurationSeconds: number;
lockoutCounterReset: number; lockoutCounterResetSeconds: number;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateUaDTO = { export type TUpdateUaDTO = {
@@ -25,8 +25,8 @@ export type TUpdateUaDTO = {
accessTokenTrustedIps?: { ipAddress: string }[]; accessTokenTrustedIps?: { ipAddress: string }[];
lockoutEnabled?: boolean; lockoutEnabled?: boolean;
lockoutThreshold?: number; lockoutThreshold?: number;
lockoutDuration?: number; lockoutDurationSeconds?: number;
lockoutCounterReset?: number; lockoutCounterResetSeconds?: number;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetUaDTO = { export type TGetUaDTO = {
@@ -153,8 +153,8 @@ export const useAddIdentityUniversalAuth = () => {
accessTokenTrustedIps, accessTokenTrustedIps,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}) => { }) => {
const { const {
data: { identityUniversalAuth } data: { identityUniversalAuth }
@@ -166,8 +166,8 @@ export const useAddIdentityUniversalAuth = () => {
accessTokenTrustedIps, accessTokenTrustedIps,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}); });
return identityUniversalAuth; return identityUniversalAuth;
}, },
@@ -196,8 +196,8 @@ export const useUpdateIdentityUniversalAuth = () => {
accessTokenPeriod, accessTokenPeriod,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}) => { }) => {
const { const {
data: { identityUniversalAuth } data: { identityUniversalAuth }
@@ -210,8 +210,8 @@ export const useUpdateIdentityUniversalAuth = () => {
accessTokenPeriod, accessTokenPeriod,
lockoutEnabled, lockoutEnabled,
lockoutThreshold, lockoutThreshold,
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}); });
return identityUniversalAuth; return identityUniversalAuth;
}, },
+6 -6
View File
@@ -116,8 +116,8 @@ export type IdentityUniversalAuth = {
accessTokenPeriod: number; accessTokenPeriod: number;
lockoutEnabled: boolean; lockoutEnabled: boolean;
lockoutThreshold: number; lockoutThreshold: number;
lockoutDuration: number; lockoutDurationSeconds: number;
lockoutCounterReset: number; lockoutCounterResetSeconds: number;
}; };
export type AddIdentityUniversalAuthDTO = { export type AddIdentityUniversalAuthDTO = {
@@ -135,8 +135,8 @@ export type AddIdentityUniversalAuthDTO = {
}[]; }[];
lockoutEnabled: boolean; lockoutEnabled: boolean;
lockoutThreshold: number; lockoutThreshold: number;
lockoutDuration: number; lockoutDurationSeconds: number;
lockoutCounterReset: number; lockoutCounterResetSeconds: number;
}; };
export type UpdateIdentityUniversalAuthDTO = { export type UpdateIdentityUniversalAuthDTO = {
@@ -154,8 +154,8 @@ export type UpdateIdentityUniversalAuthDTO = {
}[]; }[];
lockoutEnabled?: boolean; lockoutEnabled?: boolean;
lockoutThreshold?: number; lockoutThreshold?: number;
lockoutDuration?: number; lockoutDurationSeconds?: number;
lockoutCounterReset?: number; lockoutCounterResetSeconds?: number;
}; };
export type DeleteIdentityUniversalAuthDTO = { export type DeleteIdentityUniversalAuthDTO = {
@@ -151,8 +151,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
accessTokenPeriod: 0, accessTokenPeriod: 0,
lockoutEnabled: true, lockoutEnabled: true,
lockoutThreshold: 3, lockoutThreshold: 3,
lockoutDuration: 300, lockoutDurationSeconds: 300,
lockoutCounterReset: 30 lockoutCounterResetSeconds: 30
}); });
handlePopUpToggle("identity", false); handlePopUpToggle("identity", false);
@@ -158,8 +158,8 @@ export const IdentityUniversalAuthForm = ({
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDuration); const lockoutDurationObj = getObjectFromSeconds(data.lockoutDurationSeconds);
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterReset); const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterResetSeconds);
reset({ reset({
accessTokenTTL: String(data.accessTokenTTL), accessTokenTTL: String(data.accessTokenTTL),
@@ -222,8 +222,11 @@ export const IdentityUniversalAuthForm = ({
try { try {
if (!identityId) return; if (!identityId) return;
const lockoutDuration = durationToSeconds(Number(lockoutDurationValue), lockoutDurationUnit); const lockoutDurationSeconds = durationToSeconds(
const lockoutCounterReset = durationToSeconds( Number(lockoutDurationValue),
lockoutDurationUnit
);
const lockoutCounterResetSeconds = durationToSeconds(
Number(lockoutCounterResetValue), Number(lockoutCounterResetValue),
lockoutCounterResetUnit lockoutCounterResetUnit
); );
@@ -241,8 +244,8 @@ export const IdentityUniversalAuthForm = ({
accessTokenPeriod: Number(accessTokenPeriod), accessTokenPeriod: Number(accessTokenPeriod),
lockoutEnabled, lockoutEnabled,
lockoutThreshold: Number(lockoutThreshold), lockoutThreshold: Number(lockoutThreshold),
lockoutDuration, lockoutDurationSeconds,
lockoutCounterReset lockoutCounterResetSeconds
}); });
} else { } else {
// create new universal auth configuration // create new universal auth configuration
@@ -258,8 +261,8 @@ export const IdentityUniversalAuthForm = ({
accessTokenPeriod: Number(accessTokenPeriod), accessTokenPeriod: Number(accessTokenPeriod),
lockoutEnabled, lockoutEnabled,
lockoutThreshold: Number(lockoutThreshold), lockoutThreshold: Number(lockoutThreshold),
lockoutDuration, lockoutDurationSeconds: Number(lockoutDurationSeconds),
lockoutCounterReset lockoutCounterResetSeconds: Number(lockoutCounterResetSeconds)
}); });
} }
@@ -1,5 +1,6 @@
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2"; import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";