mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 09:26:14 +00:00
feat(machine-identities): LDAP Auth Lockout
This commit is contained in:
@@ -4,22 +4,48 @@ import { TableName } from "../schemas";
|
|||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
||||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
|
||||||
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
|
||||||
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDuration");
|
||||||
t.integer("lockoutDuration").notNullable().defaultTo(300); // 5 minutes (in seconds)
|
const hasLockoutCounterReset = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutCounterReset");
|
||||||
t.integer("lockoutCounterReset").notNullable().defaultTo(30); // 30 seconds
|
|
||||||
|
await knex.schema.alterTable(TableName.IdentityUniversalAuth, async (t) => {
|
||||||
|
if (!hasLockoutEnabled) {
|
||||||
|
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
||||||
|
}
|
||||||
|
if (!hasLockoutThreshold) {
|
||||||
|
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
||||||
|
}
|
||||||
|
if (!hasLockoutDuration) {
|
||||||
|
t.integer("lockoutDuration").notNullable().defaultTo(300); // 5 minutes (in seconds)
|
||||||
|
}
|
||||||
|
if (!hasLockoutCounterReset) {
|
||||||
|
t.integer("lockoutCounterReset").notNullable().defaultTo(30); // 30 seconds
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
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 hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
|
||||||
|
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDuration");
|
||||||
|
const hasLockoutCounterReset = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutCounterReset");
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||||
t.dropColumn("lockoutEnabled");
|
if (hasLockoutEnabled) {
|
||||||
t.dropColumn("lockoutThreshold");
|
t.dropColumn("lockoutEnabled");
|
||||||
t.dropColumn("lockoutDuration");
|
}
|
||||||
t.dropColumn("lockoutCounterReset");
|
if (hasLockoutThreshold) {
|
||||||
|
t.dropColumn("lockoutThreshold");
|
||||||
|
}
|
||||||
|
if (hasLockoutDuration) {
|
||||||
|
t.dropColumn("lockoutDuration");
|
||||||
|
}
|
||||||
|
if (hasLockoutCounterReset) {
|
||||||
|
t.dropColumn("lockoutCounterReset");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||||
|
if (!hasLockoutEnabled) {
|
||||||
|
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
||||||
|
}
|
||||||
|
if (!hasLockoutThreshold) {
|
||||||
|
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
||||||
|
}
|
||||||
|
if (!hasLockoutDuration) {
|
||||||
|
t.integer("lockoutDuration").notNullable().defaultTo(300); // 5 minutes (in seconds)
|
||||||
|
}
|
||||||
|
if (!hasLockoutCounterReset) {
|
||||||
|
t.integer("lockoutCounterReset").notNullable().defaultTo(30); // 30 seconds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
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");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||||
|
if (hasLockoutEnabled) {
|
||||||
|
t.dropColumn("lockoutEnabled");
|
||||||
|
}
|
||||||
|
if (hasLockoutThreshold) {
|
||||||
|
t.dropColumn("lockoutThreshold");
|
||||||
|
}
|
||||||
|
if (hasLockoutDuration) {
|
||||||
|
t.dropColumn("lockoutDuration");
|
||||||
|
}
|
||||||
|
if (hasLockoutCounterReset) {
|
||||||
|
t.dropColumn("lockoutCounterReset");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,11 @@ export const IdentityLdapAuthsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
accessTokenPeriod: z.coerce.number().default(0),
|
accessTokenPeriod: z.coerce.number().default(0),
|
||||||
templateId: z.string().uuid().nullable().optional()
|
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)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export enum EventType {
|
|||||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||||
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
|
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
|
||||||
|
CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-identity-ldap-lockouts",
|
||||||
|
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
||||||
@@ -1369,6 +1370,10 @@ interface AddIdentityLdapAuthEvent {
|
|||||||
allowedFields?: TAllowedFields[];
|
allowedFields?: TAllowedFields[];
|
||||||
url: string;
|
url: string;
|
||||||
templateId?: string | null;
|
templateId?: string | null;
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDuration: number;
|
||||||
|
lockoutCounterReset: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,6 +1388,10 @@ interface UpdateIdentityLdapAuthEvent {
|
|||||||
allowedFields?: TAllowedFields[];
|
allowedFields?: TAllowedFields[];
|
||||||
url?: string;
|
url?: string;
|
||||||
templateId?: string | null;
|
templateId?: string | null;
|
||||||
|
lockoutEnabled?: boolean;
|
||||||
|
lockoutThreshold?: number;
|
||||||
|
lockoutDuration?: number;
|
||||||
|
lockoutCounterReset?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,6 +1409,13 @@ interface RevokeIdentityLdapAuthEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClearIdentityLdapAuthLockoutsEvent {
|
||||||
|
type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface LoginIdentityOidcAuthEvent {
|
interface LoginIdentityOidcAuthEvent {
|
||||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -3553,6 +3569,7 @@ export type Event =
|
|||||||
| UpdateIdentityLdapAuthEvent
|
| UpdateIdentityLdapAuthEvent
|
||||||
| GetIdentityLdapAuthEvent
|
| GetIdentityLdapAuthEvent
|
||||||
| RevokeIdentityLdapAuthEvent
|
| RevokeIdentityLdapAuthEvent
|
||||||
|
| ClearIdentityLdapAuthLockoutsEvent
|
||||||
| CreateEnvironmentEvent
|
| CreateEnvironmentEvent
|
||||||
| GetEnvironmentEvent
|
| GetEnvironmentEvent
|
||||||
| UpdateEnvironmentEvent
|
| UpdateEnvironmentEvent
|
||||||
|
|||||||
@@ -240,7 +240,11 @@ export const LDAP_AUTH = {
|
|||||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||||
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
||||||
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
|
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
|
||||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
|
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."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
identityId: "The ID of the identity to update the configuration for.",
|
identityId: "The ID of the identity to update the configuration for.",
|
||||||
@@ -255,13 +259,20 @@ export const LDAP_AUTH = {
|
|||||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||||
templateId: "The ID of the identity auth template to update the configuration to."
|
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."
|
||||||
},
|
},
|
||||||
RETRIEVE: {
|
RETRIEVE: {
|
||||||
identityId: "The ID of the identity to retrieve the configuration for."
|
identityId: "The ID of the identity to retrieve the configuration for."
|
||||||
},
|
},
|
||||||
REVOKE: {
|
REVOKE: {
|
||||||
identityId: "The ID of the identity to revoke the configuration for."
|
identityId: "The ID of the identity to revoke the configuration for."
|
||||||
|
},
|
||||||
|
CLEAR_CLIENT_LOCKOUTS: {
|
||||||
|
identityId: "The ID of the identity to clear the client lockouts from."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1612,7 +1612,8 @@ export const registerRoutes = async (
|
|||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
identityDAL,
|
identityDAL,
|
||||||
identityAuthTemplateDAL
|
identityAuthTemplateDAL,
|
||||||
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||||
|
|||||||
@@ -135,19 +135,41 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preValidation: passport.authenticate("ldapauth", {
|
preValidation: [
|
||||||
failWithError: true,
|
async (req, res) => {
|
||||||
session: false
|
await server.services.identityLdapAuth.checkLdapLockout({
|
||||||
}) as any,
|
identityId: req.body.identityId,
|
||||||
|
username: req.body.username
|
||||||
|
});
|
||||||
|
|
||||||
errorHandler: (error) => {
|
try {
|
||||||
if (error.name === "AuthenticationError") {
|
const passportRes = await (
|
||||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
passport.authenticate("ldapauth", {
|
||||||
|
failWithError: true,
|
||||||
|
session: false
|
||||||
|
}) as any
|
||||||
|
)(req, res);
|
||||||
|
|
||||||
|
await server.services.identityLdapAuth.resetLdapLockoutCounter({
|
||||||
|
identityId: req.body.identityId,
|
||||||
|
username: req.body.username
|
||||||
|
});
|
||||||
|
|
||||||
|
return passportRes;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as any).status === 401) {
|
||||||
|
await server.services.identityLdapAuth.incrementLdapLockout({
|
||||||
|
identityId: req.body.identityId,
|
||||||
|
username: req.body.username
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
|
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
if (!req.passportMachineIdentity?.identityId) {
|
if (!req.passportMachineIdentity?.identityId) {
|
||||||
throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." });
|
throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." });
|
||||||
@@ -241,7 +263,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
.int()
|
.int()
|
||||||
.min(0)
|
.min(0)
|
||||||
.default(0)
|
.default(0)
|
||||||
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.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)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
@@ -291,7 +317,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
.int()
|
.int()
|
||||||
.min(0)
|
.min(0)
|
||||||
.default(0)
|
.default(0)
|
||||||
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.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)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
@@ -331,7 +361,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||||
allowedFields: req.body.allowedFields,
|
allowedFields: req.body.allowedFields,
|
||||||
templateId: identityLdapAuth.templateId
|
templateId: identityLdapAuth.templateId,
|
||||||
|
lockoutEnabled: identityLdapAuth.lockoutEnabled,
|
||||||
|
lockoutThreshold: identityLdapAuth.lockoutThreshold,
|
||||||
|
lockoutDuration: identityLdapAuth.lockoutDuration,
|
||||||
|
lockoutCounterReset: identityLdapAuth.lockoutCounterReset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -395,7 +429,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
.max(315360000)
|
.max(315360000)
|
||||||
.min(0)
|
.min(0)
|
||||||
.optional()
|
.optional()
|
||||||
.describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL)
|
.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)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
@@ -434,7 +472,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
allowedFields: req.body.allowedFields,
|
allowedFields: req.body.allowedFields,
|
||||||
templateId: identityLdapAuth.templateId
|
templateId: identityLdapAuth.templateId,
|
||||||
|
lockoutEnabled: identityLdapAuth.lockoutEnabled,
|
||||||
|
lockoutThreshold: identityLdapAuth.lockoutThreshold,
|
||||||
|
lockoutDuration: identityLdapAuth.lockoutDuration,
|
||||||
|
lockoutCounterReset: identityLdapAuth.lockoutCounterReset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -553,4 +595,53 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
return { identityLdapAuth };
|
return { identityLdapAuth };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/ldap-auth/identities/:identityId/clear-lockouts",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.LdapAuth],
|
||||||
|
description: "Clear LDAP Auth Lockouts for identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string().describe(LDAP_AUTH.CLEAR_CLIENT_LOCKOUTS.identityId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
deleted: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const clearLockoutsData = await server.services.identityLdapAuth.clearLdapAuthLockouts({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: clearLockoutsData.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS,
|
||||||
|
metadata: {
|
||||||
|
identityId: clearLockoutsData.identityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return clearLockoutsData;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import {
|
|||||||
validatePrivilegeChangeOperation
|
validatePrivilegeChangeOperation
|
||||||
} from "@app/ee/services/permission/permission-fns";
|
} from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
@@ -32,8 +33,12 @@ import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal";
|
|||||||
import {
|
import {
|
||||||
AllowedFieldsSchema,
|
AllowedFieldsSchema,
|
||||||
TAttachLdapAuthDTO,
|
TAttachLdapAuthDTO,
|
||||||
|
TCheckLdapAuthLockoutDTO,
|
||||||
|
TClearLdapAuthLockoutsDTO,
|
||||||
TGetLdapAuthDTO,
|
TGetLdapAuthDTO,
|
||||||
|
TIncrementLdapAuthLockoutDTO,
|
||||||
TLoginLdapAuthDTO,
|
TLoginLdapAuthDTO,
|
||||||
|
TResetLdapAuthLockoutCounterDTO,
|
||||||
TRevokeLdapAuthDTO,
|
TRevokeLdapAuthDTO,
|
||||||
TUpdateLdapAuthDTO
|
TUpdateLdapAuthDTO
|
||||||
} from "./identity-ldap-auth-types";
|
} from "./identity-ldap-auth-types";
|
||||||
@@ -50,10 +55,16 @@ type TIdentityLdapAuthServiceFactoryDep = {
|
|||||||
kmsService: TKmsServiceFactory;
|
kmsService: TKmsServiceFactory;
|
||||||
identityDAL: TIdentityDALFactory;
|
identityDAL: TIdentityDALFactory;
|
||||||
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
|
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
|
||||||
|
|
||||||
|
type LockoutObject = {
|
||||||
|
lockedOut: boolean;
|
||||||
|
failedAttempts: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const identityLdapAuthServiceFactory = ({
|
export const identityLdapAuthServiceFactory = ({
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityDAL,
|
identityDAL,
|
||||||
@@ -62,7 +73,8 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
identityAuthTemplateDAL
|
identityAuthTemplateDAL,
|
||||||
|
keyStore
|
||||||
}: TIdentityLdapAuthServiceFactoryDep) => {
|
}: TIdentityLdapAuthServiceFactoryDep) => {
|
||||||
const getLdapConfig = async (identityId: string) => {
|
const getLdapConfig = async (identityId: string) => {
|
||||||
const identity = await identityDAL.findOne({ id: identityId });
|
const identity = await identityDAL.findOne({ id: identityId });
|
||||||
@@ -126,13 +138,17 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
|
||||||
if (!identityMembershipOrg) {
|
if (!identityMembershipOrg) {
|
||||||
throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
throw new UnauthorizedError({
|
||||||
|
message: "Invalid credentials"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||||
|
|
||||||
if (!identityLdapAuth) {
|
if (!identityLdapAuth) {
|
||||||
throw new NotFoundError({ message: `Failed to find LDAP auth for identity with ID ${identityId}` });
|
throw new UnauthorizedError({
|
||||||
|
message: "Invalid credentials"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||||
@@ -204,7 +220,11 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
isActorSuperAdmin,
|
isActorSuperAdmin,
|
||||||
allowedFields
|
allowedFields,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
}: TAttachLdapAuthDTO) => {
|
}: TAttachLdapAuthDTO) => {
|
||||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||||
|
|
||||||
@@ -337,7 +357,11 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
||||||
templateId
|
templateId,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -363,7 +387,11 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
}: TUpdateLdapAuthDTO) => {
|
}: TUpdateLdapAuthDTO) => {
|
||||||
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}` });
|
||||||
@@ -511,7 +539,11 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
: undefined
|
: undefined,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId };
|
return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId };
|
||||||
@@ -611,12 +643,104 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
return revokedIdentityLdapAuth;
|
return revokedIdentityLdapAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkLdapLockout = async ({ identityId, username }: TCheckLdapAuthLockoutDTO) => {
|
||||||
|
const LOCKOUT_KEY = `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:${username.trim().toLowerCase()}`;
|
||||||
|
|
||||||
|
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||||
|
|
||||||
|
if (lockoutRaw) {
|
||||||
|
const lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||||
|
|
||||||
|
if (lockout.lockedOut) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "This identity auth method is temporarily locked, please try again later"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const incrementLdapLockout = async ({ identityId, username }: TIncrementLdapAuthLockoutDTO) => {
|
||||||
|
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||||
|
if (!identityLdapAuth) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Invalid credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityLdapAuth.lockoutEnabled) {
|
||||||
|
const LOCKOUT_KEY = `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:${username.trim().toLowerCase()}`;
|
||||||
|
|
||||||
|
let lockout: LockoutObject = {
|
||||||
|
lockedOut: false,
|
||||||
|
failedAttempts: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||||
|
if (lockoutRaw) {
|
||||||
|
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockout.failedAttempts += 1;
|
||||||
|
if (lockout.failedAttempts >= identityLdapAuth.lockoutThreshold) {
|
||||||
|
lockout.lockedOut = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
LOCKOUT_KEY,
|
||||||
|
lockout.lockedOut ? identityLdapAuth.lockoutDuration : identityLdapAuth.lockoutCounterReset,
|
||||||
|
JSON.stringify(lockout)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetLdapLockoutCounter = async ({ identityId, username }: TResetLdapAuthLockoutCounterDTO) => {
|
||||||
|
await keyStore.deleteItem(
|
||||||
|
`lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:${username.trim().toLowerCase()}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLdapAuthLockouts = async ({
|
||||||
|
identityId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TClearLdapAuthLockoutsDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||||
|
|
||||||
|
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have ldap auth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const deleted = await keyStore.deleteItems({
|
||||||
|
pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:*`
|
||||||
|
});
|
||||||
|
|
||||||
|
return { deleted, identityId, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachLdapAuth,
|
attachLdapAuth,
|
||||||
getLdapConfig,
|
getLdapConfig,
|
||||||
updateLdapAuth,
|
updateLdapAuth,
|
||||||
login,
|
login,
|
||||||
revokeIdentityLdapAuth,
|
revokeIdentityLdapAuth,
|
||||||
getLdapAuth
|
getLdapAuth,
|
||||||
|
checkLdapLockout,
|
||||||
|
incrementLdapLockout,
|
||||||
|
resetLdapLockoutCounter,
|
||||||
|
clearLdapAuthLockouts
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export type TAttachLdapAuthDTO = {
|
|||||||
accessTokenNumUsesLimit: number;
|
accessTokenNumUsesLimit: number;
|
||||||
accessTokenTrustedIps: { ipAddress: string }[];
|
accessTokenTrustedIps: { ipAddress: string }[];
|
||||||
isActorSuperAdmin?: boolean;
|
isActorSuperAdmin?: boolean;
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDuration: number;
|
||||||
|
lockoutCounterReset: number;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateLdapAuthDTO = {
|
export type TUpdateLdapAuthDTO = {
|
||||||
@@ -43,6 +47,10 @@ export type TUpdateLdapAuthDTO = {
|
|||||||
accessTokenMaxTTL?: number;
|
accessTokenMaxTTL?: number;
|
||||||
accessTokenNumUsesLimit?: number;
|
accessTokenNumUsesLimit?: number;
|
||||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||||
|
lockoutEnabled?: boolean;
|
||||||
|
lockoutThreshold?: number;
|
||||||
|
lockoutDuration?: number;
|
||||||
|
lockoutCounterReset?: number;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetLdapAuthDTO = {
|
export type TGetLdapAuthDTO = {
|
||||||
@@ -56,3 +64,22 @@ export type TLoginLdapAuthDTO = {
|
|||||||
export type TRevokeLdapAuthDTO = {
|
export type TRevokeLdapAuthDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TClearLdapAuthLockoutsDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCheckLdapAuthLockoutDTO = {
|
||||||
|
identityId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIncrementLdapAuthLockoutDTO = {
|
||||||
|
identityId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TResetLdapAuthLockoutCounterDTO = {
|
||||||
|
identityId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
|
[EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
|
||||||
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved 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.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_LIST]: "List Secret Scanning Data Sources",
|
||||||
[EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source",
|
[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",
|
UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
|
||||||
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
|
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
|
||||||
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
|
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
|
||||||
|
CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-ldap-auth-lockouts",
|
||||||
|
|
||||||
CREATE_ENVIRONMENT = "create-environment",
|
CREATE_ENVIRONMENT = "create-environment",
|
||||||
UPDATE_ENVIRONMENT = "update-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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -958,7 +965,8 @@ export type Event =
|
|||||||
| GetCertificateTemplateEstConfig
|
| GetCertificateTemplateEstConfig
|
||||||
| UpdateProjectWorkflowIntegrationConfig
|
| UpdateProjectWorkflowIntegrationConfig
|
||||||
| GetProjectWorkflowIntegrationConfig
|
| GetProjectWorkflowIntegrationConfig
|
||||||
| IntegrationSyncedEvent;
|
| IntegrationSyncedEvent
|
||||||
|
| ClearIdentityLdapAuthLockoutsEvent;
|
||||||
|
|
||||||
export type AuditLog = {
|
export type AuditLog = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
AddIdentityTlsCertAuthDTO,
|
AddIdentityTlsCertAuthDTO,
|
||||||
AddIdentityTokenAuthDTO,
|
AddIdentityTokenAuthDTO,
|
||||||
AddIdentityUniversalAuthDTO,
|
AddIdentityUniversalAuthDTO,
|
||||||
|
ClearIdentityLdapAuthLockoutsDTO,
|
||||||
ClearIdentityUniversalAuthLockoutsDTO,
|
ClearIdentityUniversalAuthLockoutsDTO,
|
||||||
ClientSecretData,
|
ClientSecretData,
|
||||||
CreateIdentityDTO,
|
CreateIdentityDTO,
|
||||||
@@ -1432,7 +1433,11 @@ export const useAddIdentityLdapAuth = () => {
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
|
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||||
@@ -1448,7 +1453,11 @@ export const useAddIdentityLdapAuth = () => {
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.identityLdapAuth;
|
return data.identityLdapAuth;
|
||||||
@@ -1481,7 +1490,11 @@ export const useUpdateIdentityLdapAuth = () => {
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
|
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||||
@@ -1497,7 +1510,11 @@ export const useUpdateIdentityLdapAuth = () => {
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.identityLdapAuth;
|
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: {
|
accessTokenTrustedIps: {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDuration: number;
|
||||||
|
lockoutCounterReset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateIdentityLdapAuthDTO = {
|
export type UpdateIdentityLdapAuthDTO = {
|
||||||
@@ -625,6 +630,11 @@ export type UpdateIdentityLdapAuthDTO = {
|
|||||||
accessTokenTrustedIps?: {
|
accessTokenTrustedIps?: {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
lockoutEnabled?: boolean;
|
||||||
|
lockoutThreshold?: number;
|
||||||
|
lockoutDuration?: number;
|
||||||
|
lockoutCounterReset?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteIdentityLdapAuthDTO = {
|
export type DeleteIdentityLdapAuthDTO = {
|
||||||
@@ -650,6 +660,15 @@ export type IdentityLdapAuth = {
|
|||||||
accessTokenMaxTTL: number;
|
accessTokenMaxTTL: number;
|
||||||
accessTokenNumUsesLimit: number;
|
accessTokenNumUsesLimit: number;
|
||||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||||
|
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDuration: number;
|
||||||
|
lockoutCounterReset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClearIdentityLdapAuthLockoutsDTO = {
|
||||||
|
identityId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddIdentityTokenAuthDTO = {
|
export type AddIdentityTokenAuthDTO = {
|
||||||
|
|||||||
+103
-11
@@ -25,6 +25,7 @@ import {
|
|||||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
} from "@app/context/OrgPermissionContext/types";
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { durationToSeconds, getObjectFromSeconds } from "@app/helpers/datetime";
|
||||||
import {
|
import {
|
||||||
MachineIdentityAuthMethod,
|
MachineIdentityAuthMethod,
|
||||||
useAddIdentityLdapAuth,
|
useAddIdentityLdapAuth,
|
||||||
@@ -35,6 +36,8 @@ import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
|||||||
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
|
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { LockoutTab } from "./lockout/LockoutTab";
|
||||||
|
import { superRefineLockout } from "./lockout/super-refine";
|
||||||
import { IdentityFormTab } from "./types";
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
@@ -74,9 +77,28 @@ const schema = z
|
|||||||
ipAddress: z.string().max(50)
|
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) => {
|
.superRefine((data, ctx) => {
|
||||||
|
superRefineLockout(data, ctx);
|
||||||
|
|
||||||
// Validation based on scope
|
// Validation based on scope
|
||||||
if (data.scope === "template") {
|
if (data.scope === "template") {
|
||||||
if (!data.templateId) {
|
if (!data.templateId) {
|
||||||
@@ -178,12 +200,25 @@ export const IdentityLdapAuthForm = ({
|
|||||||
accessTokenTTL: "2592000",
|
accessTokenTTL: "2592000",
|
||||||
accessTokenMaxTTL: "2592000",
|
accessTokenMaxTTL: "2592000",
|
||||||
accessTokenNumUsesLimit: "0",
|
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 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 {
|
const {
|
||||||
fields: accessTokenTrustedIpsFields,
|
fields: accessTokenTrustedIpsFields,
|
||||||
append: appendAccessTokenTrustedIp,
|
append: appendAccessTokenTrustedIp,
|
||||||
@@ -210,6 +245,9 @@ export const IdentityLdapAuthForm = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
const detectedScope = determineScope(data);
|
const detectedScope = determineScope(data);
|
||||||
|
|
||||||
|
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDuration);
|
||||||
|
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterReset);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
scope: detectedScope,
|
scope: detectedScope,
|
||||||
templateId: data.templateId || "",
|
templateId: data.templateId || "",
|
||||||
@@ -229,7 +267,13 @@ export const IdentityLdapAuthForm = ({
|
|||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -247,7 +291,13 @@ export const IdentityLdapAuthForm = ({
|
|||||||
accessTokenTTL: "2592000",
|
accessTokenTTL: "2592000",
|
||||||
accessTokenMaxTTL: "2592000",
|
accessTokenMaxTTL: "2592000",
|
||||||
accessTokenNumUsesLimit: "0",
|
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]);
|
}, [data, reset]);
|
||||||
|
|
||||||
@@ -275,9 +325,21 @@ export const IdentityLdapAuthForm = ({
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationValue,
|
||||||
|
lockoutDurationUnit,
|
||||||
|
lockoutCounterResetValue,
|
||||||
|
lockoutCounterResetUnit
|
||||||
} = formData;
|
} = formData;
|
||||||
|
|
||||||
|
const lockoutDuration = durationToSeconds(Number(lockoutDurationValue), lockoutDurationUnit);
|
||||||
|
const lockoutCounterReset = durationToSeconds(
|
||||||
|
Number(lockoutCounterResetValue),
|
||||||
|
lockoutCounterResetUnit
|
||||||
|
);
|
||||||
|
|
||||||
const basePayload = {
|
const basePayload = {
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId,
|
||||||
@@ -287,7 +349,11 @@ export const IdentityLdapAuthForm = ({
|
|||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold: Number(lockoutThreshold),
|
||||||
|
lockoutDuration,
|
||||||
|
lockoutCounterReset
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add scope-specific fields
|
// Add scope-specific fields
|
||||||
@@ -327,7 +393,10 @@ export const IdentityLdapAuthForm = ({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
setTabValue(
|
const firstErrorField = Object.keys(fields)[0];
|
||||||
|
let tab = IdentityFormTab.Advanced;
|
||||||
|
|
||||||
|
if (
|
||||||
[
|
[
|
||||||
"scope",
|
"scope",
|
||||||
"templateId",
|
"templateId",
|
||||||
@@ -340,15 +409,29 @@ export const IdentityLdapAuthForm = ({
|
|||||||
"allowedFields",
|
"allowedFields",
|
||||||
"accessTokenMaxTTL",
|
"accessTokenMaxTTL",
|
||||||
"accessTokenNumUsesLimit"
|
"accessTokenNumUsesLimit"
|
||||||
].includes(Object.keys(fields)[0])
|
].includes(firstErrorField)
|
||||||
? IdentityFormTab.Configuration
|
) {
|
||||||
: IdentityFormTab.Advanced
|
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)}>
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
|
<Tab value={IdentityFormTab.Lockout}>Lockout</Tab>
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
@@ -691,6 +774,15 @@ export const IdentityLdapAuthForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<LockoutTab
|
||||||
|
control={control}
|
||||||
|
lockoutEnabled={lockoutEnabledWatch}
|
||||||
|
lockoutThreshold={lockoutThresholdWatch}
|
||||||
|
lockoutDurationValue={lockoutDurationValueWatch}
|
||||||
|
lockoutDurationUnit={lockoutDurationUnitWatch}
|
||||||
|
lockoutCounterResetValue={lockoutCounterResetValueWatch}
|
||||||
|
lockoutCounterResetUnit={lockoutCounterResetUnitWatch}
|
||||||
|
/>
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+14
-243
@@ -11,9 +11,6 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Switch,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
@@ -29,6 +26,8 @@ import {
|
|||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { LockoutTab } from "./lockout/LockoutTab";
|
||||||
|
import { superRefineLockout } from "./lockout/super-refine";
|
||||||
import { IdentityFormTab } from "./types";
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
@@ -82,63 +81,7 @@ const schema = z
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.required()
|
.required()
|
||||||
.superRefine((data, ctx) => {
|
.superRefine(superRefineLockout);
|
||||||
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"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
@@ -315,8 +258,8 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenPeriod: Number(accessTokenPeriod),
|
accessTokenPeriod: Number(accessTokenPeriod),
|
||||||
lockoutEnabled,
|
lockoutEnabled,
|
||||||
lockoutThreshold: Number(lockoutThreshold),
|
lockoutThreshold: Number(lockoutThreshold),
|
||||||
lockoutDuration: Number(lockoutDuration),
|
lockoutDuration,
|
||||||
lockoutCounterReset: Number(lockoutCounterReset)
|
lockoutCounterReset
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,187 +378,15 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={IdentityFormTab.Lockout}>
|
<LockoutTab
|
||||||
<div className="mb-3 flex flex-col">
|
control={control}
|
||||||
<Controller
|
lockoutEnabled={lockoutEnabledWatch}
|
||||||
control={control}
|
lockoutThreshold={lockoutThresholdWatch}
|
||||||
name="lockoutEnabled"
|
lockoutDurationValue={lockoutDurationValueWatch}
|
||||||
defaultValue
|
lockoutDurationUnit={lockoutDurationUnitWatch}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
lockoutCounterResetValue={lockoutCounterResetValueWatch}
|
||||||
return (
|
lockoutCounterResetUnit={lockoutCounterResetUnitWatch}
|
||||||
<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>
|
|
||||||
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
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 { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
|
||||||
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||||
|
import { LockoutFields } from "./IdentityAuthLockoutFields";
|
||||||
import { ViewAuthMethodProps } from "./types";
|
import { ViewAuthMethodProps } from "./types";
|
||||||
|
|
||||||
export const ViewIdentityLdapAuthContent = ({
|
export const ViewIdentityLdapAuthContent = ({
|
||||||
@@ -14,9 +15,11 @@ export const ViewIdentityLdapAuthContent = ({
|
|||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
onDelete,
|
onDelete,
|
||||||
popUp
|
popUp,
|
||||||
|
lockedOut
|
||||||
}: ViewAuthMethodProps) => {
|
}: ViewAuthMethodProps) => {
|
||||||
const { data, isPending } = useGetIdentityLdapAuth(identityId);
|
const { data, isPending } = useGetIdentityLdapAuth(identityId);
|
||||||
|
const clearLockoutsResult = useClearIdentityLdapAuthLockouts();
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
@@ -98,6 +101,12 @@ export const ViewIdentityLdapAuthContent = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</IdentityAuthFieldDisplay>
|
</IdentityAuthFieldDisplay>
|
||||||
|
<LockoutFields
|
||||||
|
identityId={identityId}
|
||||||
|
lockedOut={lockedOut}
|
||||||
|
clearLockoutsResult={clearLockoutsResult}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
</ViewIdentityContentWrapper>
|
</ViewIdentityContentWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-54
@@ -1,11 +1,7 @@
|
|||||||
import { useState } from "react";
|
|
||||||
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 { createNotification } from "@app/components/notifications";
|
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Button, EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
|
||||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useClearIdentityUniversalAuthLockouts,
|
useClearIdentityUniversalAuthLockouts,
|
||||||
@@ -15,6 +11,7 @@ import {
|
|||||||
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
|
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||||
|
import { LockoutFields } from "./IdentityAuthLockoutFields";
|
||||||
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
|
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
|
||||||
import { ViewAuthMethodProps } from "./types";
|
import { ViewAuthMethodProps } from "./types";
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
||||||
@@ -30,32 +27,12 @@ export const ViewIdentityUniversalAuthContent = ({
|
|||||||
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
||||||
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
||||||
useGetIdentityUniversalAuthClientSecrets(identityId);
|
useGetIdentityUniversalAuthClientSecrets(identityId);
|
||||||
const { mutateAsync: clearLockoutsFn, isPending: isClearLockoutsPending } =
|
const clearLockoutsResult = useClearIdentityUniversalAuthLockouts();
|
||||||
useClearIdentityUniversalAuthLockouts();
|
|
||||||
|
|
||||||
const [lockedOutState, setLockedOutState] = useState(lockedOut);
|
|
||||||
|
|
||||||
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
||||||
initialState: "Copy Client ID to clipboard"
|
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) {
|
if (isPending || clientSecretsPending) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-center">
|
<div className="flex w-full items-center justify-center">
|
||||||
@@ -112,34 +89,12 @@ export const ViewIdentityUniversalAuthContent = ({
|
|||||||
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
||||||
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||||
</IdentityAuthFieldDisplay>
|
</IdentityAuthFieldDisplay>
|
||||||
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
|
<LockoutFields
|
||||||
<span className="text-bunker-300">Lockout Options</span>
|
identityId={identityId}
|
||||||
<OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
|
lockedOut={lockedOut}
|
||||||
{(isAllowed) => (
|
clearLockoutsResult={clearLockoutsResult}
|
||||||
<Button
|
data={data}
|
||||||
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>
|
|
||||||
<div className="col-span-2 my-3">
|
<div className="col-span-2 my-3">
|
||||||
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
||||||
<span className="text-bunker-300">Client ID</span>
|
<span className="text-bunker-300">Client ID</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user