mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 20:29:01 +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> {
|
||||
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
||||
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
||||
t.integer("lockoutDuration").notNullable().defaultTo(300); // 5 minutes (in seconds)
|
||||
t.integer("lockoutCounterReset").notNullable().defaultTo(30); // 30 seconds
|
||||
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, 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> {
|
||||
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) => {
|
||||
t.dropColumn("lockoutEnabled");
|
||||
t.dropColumn("lockoutThreshold");
|
||||
t.dropColumn("lockoutDuration");
|
||||
t.dropColumn("lockoutCounterReset");
|
||||
if (hasLockoutEnabled) {
|
||||
t.dropColumn("lockoutEnabled");
|
||||
}
|
||||
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(),
|
||||
updatedAt: z.date(),
|
||||
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>;
|
||||
|
||||
@@ -199,6 +199,7 @@ export enum EventType {
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-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_LDAP_AUTH_LOCKOUTS = "clear-identity-ldap-lockouts",
|
||||
|
||||
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",
|
||||
@@ -1369,6 +1370,10 @@ interface AddIdentityLdapAuthEvent {
|
||||
allowedFields?: TAllowedFields[];
|
||||
url: string;
|
||||
templateId?: string | null;
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDuration: number;
|
||||
lockoutCounterReset: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1383,6 +1388,10 @@ interface UpdateIdentityLdapAuthEvent {
|
||||
allowedFields?: TAllowedFields[];
|
||||
url?: string;
|
||||
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 {
|
||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
@@ -3553,6 +3569,7 @@ export type Event =
|
||||
| UpdateIdentityLdapAuthEvent
|
||||
| GetIdentityLdapAuthEvent
|
||||
| RevokeIdentityLdapAuthEvent
|
||||
| ClearIdentityLdapAuthLockoutsEvent
|
||||
| CreateEnvironmentEvent
|
||||
| GetEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
|
||||
@@ -240,7 +240,11 @@ export const LDAP_AUTH = {
|
||||
accessTokenTTL: "The 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.",
|
||||
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: {
|
||||
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.",
|
||||
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.",
|
||||
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: {
|
||||
identityId: "The ID of the identity to retrieve the configuration for."
|
||||
},
|
||||
REVOKE: {
|
||||
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;
|
||||
|
||||
|
||||
@@ -1612,7 +1612,8 @@ export const registerRoutes = async (
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
identityDAL,
|
||||
identityAuthTemplateDAL
|
||||
identityAuthTemplateDAL,
|
||||
keyStore
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||
|
||||
@@ -135,19 +135,41 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
})
|
||||
}
|
||||
},
|
||||
preValidation: passport.authenticate("ldapauth", {
|
||||
failWithError: true,
|
||||
session: false
|
||||
}) as any,
|
||||
preValidation: [
|
||||
async (req, res) => {
|
||||
await server.services.identityLdapAuth.checkLdapLockout({
|
||||
identityId: req.body.identityId,
|
||||
username: req.body.username
|
||||
});
|
||||
|
||||
errorHandler: (error) => {
|
||||
if (error.name === "AuthenticationError") {
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
try {
|
||||
const passportRes = await (
|
||||
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) => {
|
||||
if (!req.passportMachineIdentity?.identityId) {
|
||||
throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." });
|
||||
@@ -241,7 +263,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
.int()
|
||||
.min(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(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
@@ -291,7 +317,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
.int()
|
||||
.min(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(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
@@ -331,7 +361,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
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)
|
||||
.min(0)
|
||||
.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(
|
||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||
@@ -434,7 +472,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
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 };
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
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 { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@@ -32,8 +33,12 @@ import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal";
|
||||
import {
|
||||
AllowedFieldsSchema,
|
||||
TAttachLdapAuthDTO,
|
||||
TCheckLdapAuthLockoutDTO,
|
||||
TClearLdapAuthLockoutsDTO,
|
||||
TGetLdapAuthDTO,
|
||||
TIncrementLdapAuthLockoutDTO,
|
||||
TLoginLdapAuthDTO,
|
||||
TResetLdapAuthLockoutCounterDTO,
|
||||
TRevokeLdapAuthDTO,
|
||||
TUpdateLdapAuthDTO
|
||||
} from "./identity-ldap-auth-types";
|
||||
@@ -50,10 +55,16 @@ type TIdentityLdapAuthServiceFactoryDep = {
|
||||
kmsService: TKmsServiceFactory;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
||||
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems">;
|
||||
};
|
||||
|
||||
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
|
||||
|
||||
type LockoutObject = {
|
||||
lockedOut: boolean;
|
||||
failedAttempts: number;
|
||||
};
|
||||
|
||||
export const identityLdapAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
@@ -62,7 +73,8 @@ export const identityLdapAuthServiceFactory = ({
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService,
|
||||
identityAuthTemplateDAL
|
||||
identityAuthTemplateDAL,
|
||||
keyStore
|
||||
}: TIdentityLdapAuthServiceFactoryDep) => {
|
||||
const getLdapConfig = async (identityId: string) => {
|
||||
const identity = await identityDAL.findOne({ id: identityId });
|
||||
@@ -126,13 +138,17 @@ export const identityLdapAuthServiceFactory = ({
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
|
||||
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 });
|
||||
|
||||
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);
|
||||
@@ -204,7 +220,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin,
|
||||
allowedFields
|
||||
allowedFields,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
}: TAttachLdapAuthDTO) => {
|
||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||
|
||||
@@ -337,7 +357,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
||||
templateId
|
||||
templateId,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -363,7 +387,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
}: TUpdateLdapAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
@@ -511,7 +539,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
: undefined,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDuration,
|
||||
lockoutCounterReset
|
||||
});
|
||||
|
||||
return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId };
|
||||
@@ -611,12 +643,104 @@ export const identityLdapAuthServiceFactory = ({
|
||||
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 {
|
||||
attachLdapAuth,
|
||||
getLdapConfig,
|
||||
updateLdapAuth,
|
||||
login,
|
||||
revokeIdentityLdapAuth,
|
||||
getLdapAuth
|
||||
getLdapAuth,
|
||||
checkLdapLockout,
|
||||
incrementLdapLockout,
|
||||
resetLdapLockoutCounter,
|
||||
clearLdapAuthLockouts
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,6 +27,10 @@ export type TAttachLdapAuthDTO = {
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDuration: number;
|
||||
lockoutCounterReset: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateLdapAuthDTO = {
|
||||
@@ -43,6 +47,10 @@ export type TUpdateLdapAuthDTO = {
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
lockoutEnabled?: boolean;
|
||||
lockoutThreshold?: number;
|
||||
lockoutDuration?: number;
|
||||
lockoutCounterReset?: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetLdapAuthDTO = {
|
||||
@@ -56,3 +64,22 @@ export type TLoginLdapAuthDTO = {
|
||||
export type TRevokeLdapAuthDTO = {
|
||||
identityId: string;
|
||||
} & 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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user