diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 6ec542c6b..748a7d431 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -66,6 +66,8 @@ import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-a
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
+import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
+import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
@@ -146,6 +148,13 @@ declare module "fastify" {
providerAuthToken: string;
externalProviderAccessToken?: string;
};
+ passportMachineIdentity: {
+ identityId: string;
+ user: {
+ uid: string;
+ mail?: string;
+ };
+ };
kmipUser: {
projectId: string;
clientId: string;
@@ -153,7 +162,9 @@ declare module "fastify" {
};
auditLogInfo: Pick;
ssoConfig: Awaited>;
- ldapConfig: Awaited>;
+ ldapConfig: Awaited> & {
+ allowedFields?: TAllowedFields[];
+ };
}
interface FastifyInstance {
@@ -199,6 +210,7 @@ declare module "fastify" {
identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory;
+ identityLdapAuth: TIdentityLdapAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 13f3bc306..c26f1128e 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -432,6 +432,11 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
+import {
+ TIdentityLdapAuths,
+ TIdentityLdapAuthsInsert,
+ TIdentityLdapAuthsUpdate
+} from "@app/db/schemas/identity-ldap-auths";
import {
TMicrosoftTeamsIntegrations,
TMicrosoftTeamsIntegrationsInsert,
@@ -735,6 +740,11 @@ declare module "knex/types/tables" {
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate
>;
+ [TableName.IdentityLdapAuth]: KnexOriginal.CompositeTableType<
+ TIdentityLdapAuths,
+ TIdentityLdapAuthsInsert,
+ TIdentityLdapAuthsUpdate
+ >;
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
diff --git a/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts b/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts
new file mode 100644
index 000000000..da9912022
--- /dev/null
+++ b/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts
@@ -0,0 +1,39 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.IdentityLdapAuth))) {
+ await knex.schema.createTable(TableName.IdentityLdapAuth, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+
+ t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
+ t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
+ t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
+ t.jsonb("accessTokenTrustedIps").notNullable();
+
+ t.uuid("identityId").notNullable().unique();
+ t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
+
+ t.binary("encryptedBindDN").notNullable();
+ t.binary("encryptedBindPass").notNullable();
+ t.binary("encryptedLdapCaCertificate").nullable();
+
+ t.string("url").notNullable();
+ t.string("searchBase").notNullable();
+ t.string("searchFilter").notNullable();
+
+ t.jsonb("allowedFields").nullable();
+
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.IdentityLdapAuth);
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.IdentityLdapAuth);
+ await dropOnUpdateTrigger(knex, TableName.IdentityLdapAuth);
+}
diff --git a/backend/src/db/schemas/identity-ldap-auths.ts b/backend/src/db/schemas/identity-ldap-auths.ts
new file mode 100644
index 000000000..d5b15fc6a
--- /dev/null
+++ b/backend/src/db/schemas/identity-ldap-auths.ts
@@ -0,0 +1,32 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const IdentityLdapAuthsSchema = z.object({
+ id: z.string().uuid(),
+ accessTokenTTL: z.coerce.number().default(7200),
+ accessTokenMaxTTL: z.coerce.number().default(7200),
+ accessTokenNumUsesLimit: z.coerce.number().default(0),
+ accessTokenTrustedIps: z.unknown(),
+ identityId: z.string().uuid(),
+ encryptedBindDN: zodBuffer,
+ encryptedBindPass: zodBuffer,
+ encryptedLdapCaCertificate: zodBuffer.nullable().optional(),
+ url: z.string(),
+ searchBase: z.string(),
+ searchFilter: z.string(),
+ allowedFields: z.unknown().nullable().optional(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TIdentityLdapAuths = z.infer;
+export type TIdentityLdapAuthsInsert = Omit, TImmutableDBKeys>;
+export type TIdentityLdapAuthsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index 60a13bac6..81d5319e1 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -80,6 +80,7 @@ export enum TableName {
IdentityAwsAuth = "identity_aws_auths",
IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths",
+ IdentityLdapAuth = "identity_ldap_auths",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
@@ -232,7 +233,8 @@ export enum IdentityAuthMethod {
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth",
- JWT_AUTH = "jwt-auth"
+ JWT_AUTH = "jwt-auth",
+ LDAP_AUTH = "ldap-auth"
}
export enum ProjectType {
diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index d7cad74be..a2caf2bff 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -34,6 +34,7 @@ import { WorkflowIntegration } from "@app/services/workflow-integration/workflow
import { KmipPermission } from "../kmip/kmip-enum";
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
+import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
export type TListProjectAuditLogDTO = {
filter: {
@@ -119,44 +120,60 @@ export enum EventType {
CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth",
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
+
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth",
+
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
+
LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth",
ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth",
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
+
LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth",
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth",
+
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-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",
+
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
+
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
+
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth",
+
+ LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
+ ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",
+ UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
+ GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
+ REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
+
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
@@ -1034,6 +1051,55 @@ interface GetIdentityAzureAuthEvent {
};
}
+interface LoginIdentityLdapAuthEvent {
+ type: EventType.LOGIN_IDENTITY_LDAP_AUTH;
+ metadata: {
+ identityId: string;
+ ldapUsername: string;
+ ldapEmail?: string;
+ };
+}
+
+interface AddIdentityLdapAuthEvent {
+ type: EventType.ADD_IDENTITY_LDAP_AUTH;
+ metadata: {
+ identityId: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: Array;
+ allowedFields?: TAllowedFields[];
+ url: string;
+ };
+}
+
+interface UpdateIdentityLdapAuthEvent {
+ type: EventType.UPDATE_IDENTITY_LDAP_AUTH;
+ metadata: {
+ identityId: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: Array;
+ allowedFields?: TAllowedFields[];
+ url?: string;
+ };
+}
+
+interface GetIdentityLdapAuthEvent {
+ type: EventType.GET_IDENTITY_LDAP_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface RevokeIdentityLdapAuthEvent {
+ type: EventType.REVOKE_IDENTITY_LDAP_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface LoginIdentityOidcAuthEvent {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
metadata: {
@@ -2785,6 +2851,11 @@ export type Event =
| UpdateIdentityJwtAuthEvent
| GetIdentityJwtAuthEvent
| DeleteIdentityJwtAuthEvent
+ | LoginIdentityLdapAuthEvent
+ | AddIdentityLdapAuthEvent
+ | UpdateIdentityLdapAuthEvent
+ | GetIdentityLdapAuthEvent
+ | RevokeIdentityLdapAuthEvent
| CreateEnvironmentEvent
| GetEnvironmentEvent
| UpdateEnvironmentEvent
diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts
index 86f4bf0d5..941335fa4 100644
--- a/backend/src/ee/services/ldap-config/ldap-config-types.ts
+++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts
@@ -14,6 +14,11 @@ export type TLDAPConfig = {
caCert: string;
};
+export type TTestLDAPConfigDTO = Omit<
+ TLDAPConfig,
+ "organization" | "id" | "groupSearchBase" | "groupSearchFilter" | "isActive" | "uniqueUserAttribute" | "searchBase"
+>;
+
export type TCreateLdapCfgDTO = {
orgId: string;
isActive: boolean;
diff --git a/backend/src/ee/services/ldap-config/ldap-fns.ts b/backend/src/ee/services/ldap-config/ldap-fns.ts
index 44af718ed..01b70b4db 100644
--- a/backend/src/ee/services/ldap-config/ldap-fns.ts
+++ b/backend/src/ee/services/ldap-config/ldap-fns.ts
@@ -2,15 +2,14 @@ import ldapjs from "ldapjs";
import { logger } from "@app/lib/logger";
-import { TLDAPConfig } from "./ldap-config-types";
+import { TLDAPConfig, TTestLDAPConfigDTO } from "./ldap-config-types";
export const isValidLdapFilter = (filter: string) => {
try {
ldapjs.parseFilter(filter);
return true;
} catch (error) {
- logger.error("Invalid LDAP filter");
- logger.error(error);
+ logger.error(error, "Invalid LDAP filter");
return false;
}
};
@@ -20,7 +19,7 @@ export const isValidLdapFilter = (filter: string) => {
* @param ldapConfig - The LDAP configuration to test
* @returns {Boolean} isConnected - Whether or not the connection was successful
*/
-export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise => {
+export const testLDAPConfig = async (ldapConfig: TTestLDAPConfigDTO): Promise => {
return new Promise((resolve) => {
const ldapClient = ldapjs.createClient({
url: ldapConfig.url,
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 36ac43aea..9695afe35 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -18,6 +18,7 @@ export enum ApiDocsTags {
KubernetesAuth = "Kubernetes Auth",
JwtAuth = "JWT Auth",
OidcAuth = "OIDC Auth",
+ LdapAuth = "LDAP Auth",
Groups = "Groups",
Organizations = "Organizations",
Projects = "Projects",
@@ -184,6 +185,49 @@ export const UNIVERSAL_AUTH = {
}
} as const;
+export const LDAP_AUTH = {
+ LOGIN: {
+ identityId: "The ID of the identity to login.",
+ username: "The username of the LDAP user to login.",
+ password: "The password of the LDAP user to login."
+ },
+ ATTACH: {
+ identityId: "The ID of the identity to attach the configuration onto.",
+ url: "The URL of the LDAP server.",
+ allowedFields:
+ "The comma-separated array of key/value pairs of required fields that the LDAP entry must have in order to authenticate.",
+ searchBase: "The base DN to search for the LDAP user.",
+ searchFilter: "The filter to use to search for the LDAP user.",
+ bindDN: "The DN of the user to bind to the LDAP server.",
+ bindPass: "The password of the user to bind to the LDAP server.",
+ ldapCaCertificate: "The PEM-encoded CA certificate for the LDAP server.",
+ 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."
+ },
+ UPDATE: {
+ identityId: "The ID of the identity to update the configuration for.",
+ url: "The new URL of the LDAP server.",
+ allowedFields: "The comma-separated list of allowed fields to return from the LDAP user.",
+ searchBase: "The new base DN to search for the LDAP user.",
+ searchFilter: "The new filter to use to search for the LDAP user.",
+ bindDN: "The new DN of the user to bind to the LDAP server.",
+ bindPass: "The new password of the user to bind to the LDAP server.",
+ ldapCaCertificate: "The new PEM-encoded CA certificate for the LDAP server.",
+ accessTokenTTL: "The new 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.",
+ accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
+ },
+ RETRIEVE: {
+ identityId: "The ID of the identity to retrieve the configuration for."
+ },
+ REVOKE: {
+ identityId: "The ID of the identity to revoke the configuration for."
+ }
+} as const;
+
export const AWS_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts
index 170a0285f..afde8ef97 100644
--- a/backend/src/lib/logger/logger.ts
+++ b/backend/src/lib/logger/logger.ts
@@ -84,7 +84,9 @@ const redactedKeys = [
"secrets",
"key",
"password",
- "config"
+ "config",
+ "bindPass",
+ "bindDN"
];
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index e3e5ffc2a..8606bf0f2 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -160,6 +160,8 @@ import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/ident
import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
+import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
+import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@@ -354,6 +356,7 @@ export const registerRoutes = async (
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
+ const identityLdapAuthDAL = identityLdapAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(auditLogDb ?? db);
const auditLogStreamDAL = auditLogStreamDALFactory(db);
@@ -1445,6 +1448,16 @@ export const registerRoutes = async (
kmsService
});
+ const identityLdapAuthService = identityLdapAuthServiceFactory({
+ identityLdapAuthDAL,
+ permissionService,
+ kmsService,
+ identityAccessTokenDAL,
+ identityOrgMembershipDAL,
+ licenseService,
+ identityDAL
+ });
+
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
@@ -1705,6 +1718,7 @@ export const registerRoutes = async (
identityAzureAuth: identityAzureAuthService,
identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService,
+ identityLdapAuth: identityLdapAuthService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
secretApprovalPolicy: secretApprovalPolicyService,
diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts
new file mode 100644
index 000000000..3da8a425b
--- /dev/null
+++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts
@@ -0,0 +1,497 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-unsafe-return */
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+// All the any rules are disabled because passport typesense with fastify is really poor
+
+import { Authenticator } from "@fastify/passport";
+import fastifySession from "@fastify/session";
+import { FastifyRequest } from "fastify";
+import { IncomingMessage } from "http";
+import LdapStrategy from "passport-ldapauth";
+import { z } from "zod";
+
+import { IdentityLdapAuthsSchema } from "@app/db/schemas/identity-ldap-auths";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { isValidLdapFilter } from "@app/ee/services/ldap-config/ldap-fns";
+import { ApiDocsTags, LDAP_AUTH } from "@app/lib/api-docs";
+import { getConfig } from "@app/lib/config/env";
+import { UnauthorizedError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
+import { AllowedFieldsSchema } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
+import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
+
+export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) => {
+ const appCfg = getConfig();
+ const passport = new Authenticator({ key: "ldap-identity-auth", userProperty: "passportMachineIdentity" });
+ await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY });
+ await server.register(passport.initialize());
+ await server.register(passport.secureSession());
+
+ const getLdapPassportOpts = (req: FastifyRequest, done: any) => {
+ const { identityId } = req.body as {
+ identityId: string;
+ };
+
+ process.nextTick(async () => {
+ try {
+ const { ldapConfig, opts } = await server.services.identityLdapAuth.getLdapConfig(identityId);
+ req.ldapConfig = {
+ ...ldapConfig,
+ isActive: true,
+ groupSearchBase: "",
+ uniqueUserAttribute: "",
+ groupSearchFilter: ""
+ };
+
+ done(null, opts);
+ } catch (err) {
+ logger.error(err, "Error in LDAP verification callback");
+ done(err);
+ }
+ });
+ };
+
+ passport.use(
+ new LdapStrategy(
+ getLdapPassportOpts as any,
+ // eslint-disable-next-line
+ async (req: IncomingMessage, user, cb) => {
+ try {
+ const requestBody = (req as unknown as FastifyRequest).body as {
+ username: string;
+ password: string;
+ identityId: string;
+ };
+
+ if (!requestBody.username || !requestBody.password) {
+ return cb(new UnauthorizedError({ message: "Invalid request. Missing username or password." }), false);
+ }
+
+ if (!requestBody.identityId) {
+ return cb(new UnauthorizedError({ message: "Invalid request. Missing identity ID." }), false);
+ }
+
+ const { ldapConfig } = req as unknown as FastifyRequest;
+
+ if (ldapConfig.allowedFields) {
+ for (const field of ldapConfig.allowedFields) {
+ if (!user[field.key]) {
+ return cb(
+ new UnauthorizedError({ message: `Invalid request. Missing field ${field.key} on user.` }),
+ false
+ );
+ }
+
+ const value = field.value.split(",");
+
+ if (!value.includes(user[field.key])) {
+ return cb(
+ new UnauthorizedError({
+ message: `Invalid request. User field '${field.key}' does not match required fields.`
+ }),
+ false
+ );
+ }
+ }
+ }
+
+ return cb(null, { identityId: requestBody.identityId, user });
+ } catch (error) {
+ logger.error(error, "Error in LDAP verification callback");
+ return cb(error, false);
+ }
+ }
+ )
+ );
+
+ server.route({
+ method: "POST",
+ url: "/ldap-auth/login",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.LdapAuth],
+ description: "Login with LDAP Auth",
+ body: z.object({
+ identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId),
+ username: z.string().describe(LDAP_AUTH.LOGIN.username),
+ password: z.string().describe(LDAP_AUTH.LOGIN.password)
+ }),
+ response: {
+ 200: z.object({
+ accessToken: z.string(),
+ expiresIn: z.coerce.number(),
+ accessTokenMaxTTL: z.coerce.number(),
+ tokenType: z.literal("Bearer")
+ })
+ }
+ },
+ preValidation: passport.authenticate("ldapauth", {
+ failWithError: true,
+ session: false
+ }) as any,
+
+ errorHandler: (error) => {
+ if (error.name === "AuthenticationError") {
+ throw new UnauthorizedError({ message: "Invalid credentials" });
+ }
+
+ throw error;
+ },
+
+ handler: async (req) => {
+ if (!req.passportMachineIdentity?.identityId) {
+ throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." });
+ }
+
+ const { identityId, user } = req.passportMachineIdentity;
+
+ const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({
+ identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityMembershipOrg?.orgId,
+ event: {
+ type: EventType.LOGIN_IDENTITY_LDAP_AUTH,
+ metadata: {
+ identityId,
+ ldapEmail: user.mail,
+ ldapUsername: user.uid
+ }
+ }
+ });
+
+ return {
+ accessToken,
+ tokenType: "Bearer" as const,
+ expiresIn: identityLdapAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/ldap-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.LdapAuth],
+ description: "Attach LDAP Auth configuration onto identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
+ }),
+ body: z
+ .object({
+ url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url),
+ bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN),
+ bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass),
+ searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase),
+ searchFilter: z
+ .string()
+ .trim()
+ .min(1)
+ .default("(uid={{username}})")
+ .refine(isValidLdapFilter, "Invalid LDAP search filter")
+ .describe(LDAP_AUTH.ATTACH.searchFilter),
+ allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
+ ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
+ .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
+ accessTokenTTL: z
+ .number()
+ .int()
+ .min(0)
+ .max(315360000)
+ .default(2592000)
+ .describe(LDAP_AUTH.ATTACH.accessTokenTTL),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .min(1)
+ .max(315360000)
+ .default(2592000)
+ .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
+ })
+ .refine(
+ (val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
+ "Access Token TTL cannot be greater than Access Token Max TTL."
+ ),
+ response: {
+ 200: z.object({
+ identityLdapAuth: IdentityLdapAuthsSchema.omit({
+ encryptedBindDN: true,
+ encryptedBindPass: true,
+ encryptedLdapCaCertificate: true
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityLdapAuth = await server.services.identityLdapAuth.attachLdapAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body,
+ identityId: req.params.identityId,
+ isActorSuperAdmin: isSuperAdmin(req.auth)
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.ADD_IDENTITY_LDAP_AUTH,
+ metadata: {
+ identityId: req.params.identityId,
+ url: identityLdapAuth.url,
+ accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
+ accessTokenTTL: identityLdapAuth.accessTokenTTL,
+ accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
+ allowedFields: req.body.allowedFields
+ }
+ }
+ });
+
+ return { identityLdapAuth };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/ldap-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.LdapAuth],
+ description: "Update LDAP Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(LDAP_AUTH.UPDATE.identityId)
+ }),
+ body: z
+ .object({
+ url: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.url),
+ bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN),
+ bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass),
+ searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase),
+ searchFilter: z
+ .string()
+ .trim()
+ .min(1)
+ .optional()
+ .refine((v) => v === undefined || isValidLdapFilter(v), "Invalid LDAP search filter")
+ .describe(LDAP_AUTH.UPDATE.searchFilter),
+ allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.UPDATE.allowedFields),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .optional()
+ .describe(LDAP_AUTH.UPDATE.accessTokenTrustedIps),
+ accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(LDAP_AUTH.UPDATE.accessTokenTTL),
+ accessTokenNumUsesLimit: z
+ .number()
+ .int()
+ .min(0)
+ .optional()
+ .describe(LDAP_AUTH.UPDATE.accessTokenNumUsesLimit),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .max(315360000)
+ .min(0)
+ .optional()
+ .describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL)
+ })
+ .refine(
+ (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
+ "Access Token TTL cannot be greater than Access Token Max TTL."
+ ),
+ response: {
+ 200: z.object({
+ identityLdapAuth: IdentityLdapAuthsSchema.omit({
+ encryptedBindDN: true,
+ encryptedBindPass: true,
+ encryptedLdapCaCertificate: true
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityLdapAuth = await server.services.identityLdapAuth.updateLdapAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body,
+ identityId: req.params.identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
+ metadata: {
+ identityId: req.params.identityId,
+ url: identityLdapAuth.url,
+ accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
+ accessTokenTTL: identityLdapAuth.accessTokenTTL,
+ accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
+ accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ allowedFields: req.body.allowedFields
+ }
+ }
+ });
+
+ return { identityLdapAuth };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/ldap-auth/identities/:identityId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.LdapAuth],
+ description: "Retrieve LDAP Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(LDAP_AUTH.RETRIEVE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityLdapAuth: IdentityLdapAuthsSchema.omit({
+ encryptedBindDN: true,
+ encryptedBindPass: true,
+ encryptedLdapCaCertificate: true
+ }).extend({
+ bindDN: z.string(),
+ bindPass: z.string(),
+ ldapCaCertificate: z.string().optional()
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityLdapAuth = await server.services.identityLdapAuth.getLdapAuth({
+ identityId: req.params.identityId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.GET_IDENTITY_LDAP_AUTH,
+ metadata: {
+ identityId: identityLdapAuth.identityId
+ }
+ }
+ });
+
+ return { identityLdapAuth };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/ldap-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.LdapAuth],
+ description: "Delete LDAP Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(LDAP_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityLdapAuth: IdentityLdapAuthsSchema.omit({
+ encryptedBindDN: true,
+ encryptedBindPass: true,
+ encryptedLdapCaCertificate: true
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityLdapAuth = await server.services.identityLdapAuth.revokeIdentityLdapAuth({
+ 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: req.permission.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_LDAP_AUTH,
+ metadata: {
+ identityId: identityLdapAuth.identityId
+ }
+ }
+ });
+
+ return { identityLdapAuth };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index a50299555..b1a49f815 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -19,6 +19,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
+import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -63,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter);
+ await authRouter.register(registerIdentityLdapAuthRouter);
},
{ prefix: "/auth" }
);
diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts
index 57517c706..a2a067cad 100644
--- a/backend/src/services/identity-access-token/identity-access-token-dal.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts
@@ -30,6 +30,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
.leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
.leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
+ .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
.leftJoin(
TableName.IdentityKubernetesAuth,
`${TableName.Identity}.id`,
@@ -48,6 +49,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
+ db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
db.ref("name").withSchema(TableName.Identity)
)
.first();
@@ -63,7 +65,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
- trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt
+ trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
+ trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap
};
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts
index a51d80e41..cd79981fe 100644
--- a/backend/src/services/identity-access-token/identity-access-token-service.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-service.ts
@@ -186,7 +186,8 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
- [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth
+ [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
+ [IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth
};
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-dal.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-dal.ts
new file mode 100644
index 000000000..0d998dbe9
--- /dev/null
+++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-dal.ts
@@ -0,0 +1,11 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TIdentityLdapAuthDALFactory = ReturnType;
+
+export const identityLdapAuthDALFactory = (db: TDbClient) => {
+ const ldapAuthOrm = ormify(db, TableName.IdentityLdapAuth);
+
+ return ldapAuthOrm;
+};
diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts
new file mode 100644
index 000000000..7462c9228
--- /dev/null
+++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts
@@ -0,0 +1,543 @@
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+import { ForbiddenError } from "@casl/ability";
+import jwt from "jsonwebtoken";
+
+import { IdentityAuthMethod } from "@app/db/schemas";
+import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
+import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
+import {
+ constructPermissionErrorMessage,
+ validatePrivilegeChangeOperation
+} from "@app/ee/services/permission/permission-fns";
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { getConfig } from "@app/lib/config/env";
+import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
+import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
+
+import { ActorType, AuthTokenType } from "../auth/auth-type";
+import { TIdentityDALFactory } from "../identity/identity-dal";
+import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
+import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
+import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
+import { TKmsServiceFactory } from "../kms/kms-service";
+import { KmsDataKey } from "../kms/kms-types";
+import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
+import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal";
+import {
+ AllowedFieldsSchema,
+ TAttachLdapAuthDTO,
+ TGetLdapAuthDTO,
+ TLoginLdapAuthDTO,
+ TRevokeLdapAuthDTO,
+ TUpdateLdapAuthDTO
+} from "./identity-ldap-auth-types";
+
+type TIdentityLdapAuthServiceFactoryDep = {
+ identityAccessTokenDAL: Pick;
+ identityLdapAuthDAL: Pick<
+ TIdentityLdapAuthDALFactory,
+ "findOne" | "transaction" | "create" | "updateById" | "delete"
+ >;
+ identityOrgMembershipDAL: Pick;
+ licenseService: Pick;
+ permissionService: Pick;
+ kmsService: TKmsServiceFactory;
+ identityDAL: TIdentityDALFactory;
+};
+
+export type TIdentityLdapAuthServiceFactory = ReturnType;
+
+export const identityLdapAuthServiceFactory = ({
+ identityAccessTokenDAL,
+ identityDAL,
+ identityLdapAuthDAL,
+ identityOrgMembershipDAL,
+ licenseService,
+ permissionService,
+ kmsService
+}: TIdentityLdapAuthServiceFactoryDep) => {
+ const getLdapConfig = async (identityId: string) => {
+ const identity = await identityDAL.findOne({ id: identityId });
+ if (!identity) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` });
+
+ const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identity.id });
+ if (!identityOrgMembership) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` });
+
+ const ldapAuth = await identityLdapAuthDAL.findOne({ identityId: identity.id });
+ if (!ldapAuth) throw new NotFoundError({ message: `LDAP auth with ID '${identityId}' not found` });
+
+ const parsedAllowedFields = ldapAuth.allowedFields
+ ? AllowedFieldsSchema.array().parse(ldapAuth.allowedFields)
+ : undefined;
+
+ const { decryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityOrgMembership.orgId
+ });
+
+ const bindDN = decryptor({ cipherTextBlob: ldapAuth.encryptedBindDN }).toString();
+ const bindPass = decryptor({ cipherTextBlob: ldapAuth.encryptedBindPass }).toString();
+ const ldapCaCertificate = ldapAuth.encryptedLdapCaCertificate
+ ? decryptor({ cipherTextBlob: ldapAuth.encryptedLdapCaCertificate }).toString()
+ : undefined;
+
+ const ldapConfig = {
+ id: ldapAuth.id,
+ organization: identityOrgMembership.orgId,
+ url: ldapAuth.url,
+ bindDN,
+ bindPass,
+ searchBase: ldapAuth.searchBase,
+ searchFilter: ldapAuth.searchFilter,
+ caCert: ldapCaCertificate || "",
+ allowedFields: parsedAllowedFields
+ };
+
+ const opts = {
+ server: {
+ url: ldapAuth.url,
+ bindDN,
+ bindCredentials: bindPass,
+ searchBase: ldapAuth.searchBase,
+ searchFilter: ldapAuth.searchFilter,
+ ...(ldapCaCertificate
+ ? {
+ tlsOptions: {
+ ca: [ldapCaCertificate]
+ }
+ }
+ : {})
+ },
+ passReqToCallback: true
+ };
+
+ return { opts, ldapConfig };
+ };
+
+ const login = async ({ identityId }: TLoginLdapAuthDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+
+ if (!identityMembershipOrg) {
+ throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+ }
+
+ const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
+
+ if (!identityLdapAuth) {
+ throw new NotFoundError({ message: `Failed to find LDAP auth for identity with ID ${identityId}` });
+ }
+
+ const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
+ if (!plan.ldap) {
+ throw new BadRequestError({
+ message:
+ "Failed to login to identity due to plan restriction. Upgrade plan to login to use LDAP authentication."
+ });
+ }
+
+ const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
+ const newToken = await identityAccessTokenDAL.create(
+ {
+ identityId: identityLdapAuth.identityId,
+ isAccessTokenRevoked: false,
+ accessTokenTTL: identityLdapAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
+ accessTokenNumUses: 0,
+ accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
+ authMethod: IdentityAuthMethod.LDAP_AUTH
+ },
+ tx
+ );
+ return newToken;
+ });
+
+ const appCfg = getConfig();
+ const accessToken = jwt.sign(
+ {
+ identityId: identityLdapAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
+ } as TIdentityAccessTokenJwtPayload,
+ appCfg.AUTH_SECRET,
+ // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
+ Number(identityAccessToken.accessTokenTTL) === 0
+ ? undefined
+ : {
+ expiresIn: Number(identityAccessToken.accessTokenTTL)
+ }
+ );
+
+ return { accessToken, identityLdapAuth, identityAccessToken, identityMembershipOrg };
+ };
+
+ const attachLdapAuth = async ({
+ identityId,
+ url,
+ searchBase,
+ searchFilter,
+ bindDN,
+ bindPass,
+ ldapCaCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId,
+ isActorSuperAdmin,
+ allowedFields
+ }: TAttachLdapAuthDTO) => {
+ await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
+
+ 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: "Failed to add LDAP Auth to already configured identity"
+ });
+ }
+
+ if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
+ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
+ }
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
+ const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
+
+ if (!plan.ldap) {
+ throw new BadRequestError({
+ message: "Failed to add LDAP Auth to identity due to plan restriction. Upgrade plan to add LDAP Auth."
+ });
+ }
+
+ const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
+ if (
+ !plan.ipAllowlisting &&
+ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
+ accessTokenTrustedIp.ipAddress !== "::/0"
+ )
+ throw new BadRequestError({
+ message:
+ "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
+ });
+ if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
+ throw new BadRequestError({
+ message: "The IP is not a valid IPv4, IPv6, or CIDR block"
+ });
+ return extractIPDetails(accessTokenTrustedIp.ipAddress);
+ });
+
+ if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
+
+ const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
+ const { encryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+
+ const { cipherTextBlob: encryptedBindPass } = encryptor({
+ plainText: Buffer.from(bindPass)
+ });
+
+ let encryptedLdapCaCertificate: Buffer | undefined;
+ if (ldapCaCertificate) {
+ const { cipherTextBlob: encryptedCertificate } = encryptor({
+ plainText: Buffer.from(ldapCaCertificate)
+ });
+
+ encryptedLdapCaCertificate = encryptedCertificate;
+ }
+
+ const { cipherTextBlob: encryptedBindDN } = encryptor({
+ plainText: Buffer.from(bindDN)
+ });
+
+ const isConnected = await testLDAPConfig({
+ bindDN,
+ bindPass,
+ caCert: ldapCaCertificate || "",
+ url
+ });
+
+ if (!isConnected) {
+ throw new BadRequestError({
+ message:
+ "Failed to connect to LDAP server. Please ensure that the LDAP server is running and your credentials are correct."
+ });
+ }
+
+ const doc = await identityLdapAuthDAL.create(
+ {
+ identityId: identityMembershipOrg.identityId,
+ encryptedBindDN,
+ encryptedBindPass,
+ searchBase,
+ searchFilter,
+ url,
+ encryptedLdapCaCertificate,
+ accessTokenMaxTTL,
+ accessTokenTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
+ allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined
+ },
+ tx
+ );
+ return doc;
+ });
+ return { ...identityLdapAuth, orgId: identityMembershipOrg.orgId };
+ };
+
+ const updateLdapAuth = async ({
+ identityId,
+ url,
+ searchBase,
+ searchFilter,
+ bindDN,
+ bindPass,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId
+ }: TUpdateLdapAuthDTO) => {
+ 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 NotFoundError({
+ message: "The identity does not have LDAP Auth attached"
+ });
+ }
+
+ const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
+
+ if (
+ (accessTokenMaxTTL || identityLdapAuth.accessTokenMaxTTL) > 0 &&
+ (accessTokenTTL || identityLdapAuth.accessTokenTTL) > (accessTokenMaxTTL || identityLdapAuth.accessTokenMaxTTL)
+ ) {
+ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
+ }
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+
+ const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
+
+ if (!plan.ldap) {
+ throw new BadRequestError({
+ message: "Failed to update LDAP Auth due to plan restriction. Upgrade plan to update LDAP Auth."
+ });
+ }
+
+ const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
+ if (
+ !plan.ipAllowlisting &&
+ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
+ accessTokenTrustedIp.ipAddress !== "::/0"
+ )
+ throw new BadRequestError({
+ message:
+ "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
+ });
+ if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
+ throw new BadRequestError({
+ message: "The IP is not a valid IPv4, IPv6, or CIDR block"
+ });
+ return extractIPDetails(accessTokenTrustedIp.ipAddress);
+ });
+
+ if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
+
+ const { encryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+
+ let encryptedBindPass: Buffer | undefined;
+ if (bindPass) {
+ const { cipherTextBlob: bindPassCiphertext } = encryptor({
+ plainText: Buffer.from(bindPass)
+ });
+
+ encryptedBindPass = bindPassCiphertext;
+ }
+
+ let encryptedLdapCaCertificate: Buffer | undefined;
+ if (ldapCaCertificate) {
+ const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
+ plainText: Buffer.from(ldapCaCertificate)
+ });
+
+ encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
+ }
+
+ let encryptedBindDN: Buffer | undefined;
+ if (bindDN) {
+ const { cipherTextBlob: bindDNCiphertext } = encryptor({
+ plainText: Buffer.from(bindDN)
+ });
+
+ encryptedBindDN = bindDNCiphertext;
+ }
+
+ const { ldapConfig } = await getLdapConfig(identityId);
+
+ const isConnected = await testLDAPConfig({
+ bindDN: bindDN || ldapConfig.bindDN,
+ bindPass: bindPass || ldapConfig.bindPass,
+ caCert: ldapCaCertificate || ldapConfig.caCert,
+ url: url || ldapConfig.url
+ });
+
+ if (!isConnected) {
+ throw new BadRequestError({
+ message:
+ "Failed to connect to LDAP server. Please ensure that the LDAP server is running and your credentials are correct."
+ });
+ }
+
+ const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, {
+ url,
+ searchBase,
+ searchFilter,
+ encryptedBindDN,
+ encryptedBindPass,
+ encryptedLdapCaCertificate,
+ allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
+ accessTokenMaxTTL,
+ accessTokenTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps: reformattedAccessTokenTrustedIps
+ ? JSON.stringify(reformattedAccessTokenTrustedIps)
+ : undefined
+ });
+
+ return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId };
+ };
+
+ const getLdapAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetLdapAuthDTO) => {
+ 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 attached"
+ });
+ }
+
+ const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ const { decryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+
+ const bindDN = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindDN }).toString();
+ const bindPass = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindPass }).toString();
+ const ldapCaCertificate = ldapIdentityAuth.encryptedLdapCaCertificate
+ ? decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedLdapCaCertificate }).toString()
+ : undefined;
+
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
+ return { ...ldapIdentityAuth, orgId: identityMembershipOrg.orgId, bindDN, bindPass, ldapCaCertificate };
+ };
+
+ const revokeIdentityLdapAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeLdapAuthDTO) => {
+ 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 attached"
+ });
+ }
+ const { permission, membership } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ const permissionBoundary = validatePrivilegeChangeOperation(
+ membership.shouldUseNewPrivilegeSystem,
+ OrgPermissionIdentityActions.RevokeAuth,
+ OrgPermissionSubjects.Identity,
+ permission,
+ rolePermission
+ );
+
+ if (!permissionBoundary.isValid)
+ throw new PermissionBoundaryError({
+ message: constructPermissionErrorMessage(
+ "Failed to revoke LDAP auth of identity with more privileged role",
+ membership.shouldUseNewPrivilegeSystem,
+ OrgPermissionIdentityActions.RevokeAuth,
+ OrgPermissionSubjects.Identity
+ ),
+ details: { missingPermissions: permissionBoundary.missingPermissions }
+ });
+
+ const revokedIdentityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
+ const [deletedLdapAuth] = await identityLdapAuthDAL.delete({ identityId }, tx);
+ await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.LDAP_AUTH }, tx);
+
+ return { ...deletedLdapAuth, orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityLdapAuth;
+ };
+
+ return {
+ attachLdapAuth,
+ getLdapConfig,
+ updateLdapAuth,
+ login,
+ revokeIdentityLdapAuth,
+ getLdapAuth
+ };
+};
diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts
new file mode 100644
index 000000000..0e6feb5fb
--- /dev/null
+++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts
@@ -0,0 +1,56 @@
+import { z } from "zod";
+
+import { TProjectPermission } from "@app/lib/types";
+
+export const AllowedFieldsSchema = z.object({
+ key: z.string().trim(),
+ value: z
+ .string()
+ .trim()
+ .transform((val) => val.replace(/\s/g, ""))
+});
+
+export type TAllowedFields = z.infer;
+
+export type TAttachLdapAuthDTO = {
+ identityId: string;
+ url: string;
+ searchBase: string;
+ searchFilter: string;
+ bindDN: string;
+ bindPass: string;
+ ldapCaCertificate?: string;
+ allowedFields?: TAllowedFields[];
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: { ipAddress: string }[];
+ isActorSuperAdmin?: boolean;
+} & Omit;
+
+export type TUpdateLdapAuthDTO = {
+ identityId: string;
+ url?: string;
+ searchBase?: string;
+ searchFilter?: string;
+ bindDN?: string;
+ bindPass?: string;
+ allowedFields?: TAllowedFields[];
+ ldapCaCertificate?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: { ipAddress: string }[];
+} & Omit;
+
+export type TGetLdapAuthDTO = {
+ identityId: string;
+} & Omit;
+
+export type TLoginLdapAuthDTO = {
+ identityId: string;
+};
+
+export type TRevokeLdapAuthDTO = {
+ identityId: string;
+} & Omit;
diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts
index 2d77e6544..6c77618e4 100644
--- a/backend/src/services/identity/identity-fns.ts
+++ b/backend/src/services/identity/identity-fns.ts
@@ -8,7 +8,8 @@ export const buildAuthMethods = ({
oidcId,
azureId,
tokenId,
- jwtId
+ jwtId,
+ ldapId
}: {
uaId?: string;
gcpId?: string;
@@ -18,6 +19,7 @@ export const buildAuthMethods = ({
azureId?: string;
tokenId?: string;
jwtId?: string;
+ ldapId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
@@ -27,6 +29,7 @@ export const buildAuthMethods = ({
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
- ...[jwtId ? IdentityAuthMethod.JWT_AUTH : null]
+ ...[jwtId ? IdentityAuthMethod.JWT_AUTH : null],
+ ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};
diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts
index dbae59bbe..8b5032945 100644
--- a/backend/src/services/identity/identity-org-dal.ts
+++ b/backend/src/services/identity/identity-org-dal.ts
@@ -14,6 +14,7 @@ import {
TIdentityUniversalAuths,
TOrgRoles
} from "@app/db/schemas";
+import { TIdentityLdapAuths } from "@app/db/schemas/identity-ldap-auths";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db";
@@ -81,6 +82,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityJwtAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentityLdapAuth,
+ `${TableName.IdentityOrgMembership}.identityId`,
+ `${TableName.IdentityLdapAuth}.identityId`
+ )
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
@@ -93,7 +99,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
-
+ db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity)
);
@@ -200,6 +206,12 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId",
`${TableName.IdentityJwtAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentityLdapAuth,
+ "paginatedIdentity.identityId",
+ `${TableName.IdentityLdapAuth}.identityId`
+ )
+
.select(
db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"),
@@ -217,7 +229,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
- db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
+ db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
+ db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -259,6 +272,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
oidcId,
azureId,
tokenId,
+ ldapId,
createdAt,
updatedAt
}) => ({
@@ -290,7 +304,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
oidcId,
azureId,
tokenId,
- jwtId
+ jwtId,
+ ldapId
})
}
}),
@@ -406,6 +421,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityJwtAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentityLdapAuth,
+ `${TableName.IdentityOrgMembership}.identityId`,
+ `${TableName.IdentityLdapAuth}.identityId`
+ )
.select(
db.ref("id").withSchema(TableName.IdentityOrgMembership),
db.ref("total_count").withSchema("searchedIdentities"),
@@ -424,7 +444,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
- db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
+ db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
+ db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -467,6 +488,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
oidcId,
azureId,
tokenId,
+ ldapId,
createdAt,
updatedAt
}) => ({
@@ -498,7 +520,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
oidcId,
azureId,
tokenId,
- jwtId
+ jwtId,
+ ldapId
})
}
}),
diff --git a/docs/api-reference/endpoints/ldap-auth/attach.mdx b/docs/api-reference/endpoints/ldap-auth/attach.mdx
new file mode 100644
index 000000000..512878887
--- /dev/null
+++ b/docs/api-reference/endpoints/ldap-auth/attach.mdx
@@ -0,0 +1,4 @@
+---
+title: "Attach"
+openapi: "POST /api/v1/auth/ldap-auth/identities/{identityId}"
+---
diff --git a/docs/api-reference/endpoints/ldap-auth/login.mdx b/docs/api-reference/endpoints/ldap-auth/login.mdx
new file mode 100644
index 000000000..737afb857
--- /dev/null
+++ b/docs/api-reference/endpoints/ldap-auth/login.mdx
@@ -0,0 +1,4 @@
+---
+title: "Login"
+openapi: "POST /api/v1/auth/ldap-auth/login"
+---
diff --git a/docs/api-reference/endpoints/ldap-auth/retrieve.mdx b/docs/api-reference/endpoints/ldap-auth/retrieve.mdx
new file mode 100644
index 000000000..fe4974cde
--- /dev/null
+++ b/docs/api-reference/endpoints/ldap-auth/retrieve.mdx
@@ -0,0 +1,4 @@
+---
+title: "Retrieve"
+openapi: "GET /api/v1/auth/ldap-auth/identities/{identityId}"
+---
diff --git a/docs/api-reference/endpoints/ldap-auth/revoke.mdx b/docs/api-reference/endpoints/ldap-auth/revoke.mdx
new file mode 100644
index 000000000..2ef0996fd
--- /dev/null
+++ b/docs/api-reference/endpoints/ldap-auth/revoke.mdx
@@ -0,0 +1,4 @@
+---
+title: "Revoke"
+openapi: "DELETE /api/v1/auth/ldap-auth/identities/{identityId}"
+---
diff --git a/docs/api-reference/endpoints/ldap-auth/update.mdx b/docs/api-reference/endpoints/ldap-auth/update.mdx
new file mode 100644
index 000000000..74b54efd3
--- /dev/null
+++ b/docs/api-reference/endpoints/ldap-auth/update.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/auth/ldap-auth/identities/{identityId}"
+---
diff --git a/docs/documentation/platform/identities/aws-auth.mdx b/docs/documentation/platform/identities/aws-auth.mdx
index 494606ccd..1c853957b 100644
--- a/docs/documentation/platform/identities/aws-auth.mdx
+++ b/docs/documentation/platform/identities/aws-auth.mdx
@@ -62,7 +62,7 @@ access the Infisical API using the AWS Auth authentication method.
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/azure-auth.mdx b/docs/documentation/platform/identities/azure-auth.mdx
index 03d997ffb..9576c4d0f 100644
--- a/docs/documentation/platform/identities/azure-auth.mdx
+++ b/docs/documentation/platform/identities/azure-auth.mdx
@@ -62,7 +62,7 @@ access the Infisical API using the Azure Auth authentication method.
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/gcp-auth.mdx b/docs/documentation/platform/identities/gcp-auth.mdx
index 6573544de..17dc5acd9 100644
--- a/docs/documentation/platform/identities/gcp-auth.mdx
+++ b/docs/documentation/platform/identities/gcp-auth.mdx
@@ -68,7 +68,7 @@ access the Infisical API using the GCP ID Token authentication method.
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

@@ -237,7 +237,7 @@ access the Infisical API using the GCP IAM authentication method.
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/jwt-auth.mdx b/docs/documentation/platform/identities/jwt-auth.mdx
index 3dcf12b29..339138881 100644
--- a/docs/documentation/platform/identities/jwt-auth.mdx
+++ b/docs/documentation/platform/identities/jwt-auth.mdx
@@ -57,7 +57,7 @@ In the following steps, we explore how to create and use identities to access th
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx
index 58069f09e..cfa0e861a 100644
--- a/docs/documentation/platform/identities/kubernetes-auth.mdx
+++ b/docs/documentation/platform/identities/kubernetes-auth.mdx
@@ -163,7 +163,7 @@ In the following steps, we explore how to create and use identities for your app
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/ldap-auth/general.mdx b/docs/documentation/platform/identities/ldap-auth/general.mdx
new file mode 100644
index 000000000..4a8c29298
--- /dev/null
+++ b/docs/documentation/platform/identities/ldap-auth/general.mdx
@@ -0,0 +1,92 @@
+---
+title: General
+description: "Learn how to authenticate with Infisical using LDAP."
+---
+
+
+
+ LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the Enterprise Tier. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
+
+
+**LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory.
+
+## Guide
+
+
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.
+
+ 
+
+ When creating an identity, you specify an organization level role for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
+
+ 
+
+ Now input a few details for your new identity. Here's some guidance for each field:
+
+ - Name (required): A friendly name for the identity.
+ - Role (required): A role from the Organization Roles tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
+
+ Once you've created an identity, you'll be redirected to a page where you can manage the identity.
+
+
+
+ To configure LDAP auth for your identity, press the **Add Auth Method** button on the identity's page.
+
+ 
+
+ Now select **LDAP Auth** from the list of available auth methods for the identity.
+
+ 
+
+
+ After selecting **LDAP Auth**, you'll see the form you need to fill out to configure LDAP auth for your identity. The following fields are available:
+
+ - `URL`: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` _(for connection over SSL/TLS)_, etc.
+ - `Bind DN`: The DN to bind to the LDAP server with.
+ - `Bind Pass`: The password to bind to the LDAP server with.
+ - `Search Base / DN`: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`.
+ - `User Search Filter`: Template used to construct the LDAP user search filter such as `(uid={{username}})`; use literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.
+ - `Required Attributes`: A key/value pair of attributes that must be present in the LDAP user entry for them to be authenticated. As an example, if you set key `uid` to value `user1,user2,user3`, then only users with `uid` of `user1`, `user2`, or `user3` will be able to login with this identity. Each value is a comma separated list of attributes.
+ - `CA Certificate`: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended.
+ - `Access Token TTL` _(default is 2592000 equivalent to 30 days)_: The lifetime for an access token in seconds. This value will be referenced at renewal time.
+ - `Access Token Max TTL` _(default is 2592000 equivalent to 30 days)_: The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
+ - `Access Token Max Number of Uses` _(default is 0)_: The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses.
+ - `Access Token Trusted IPs`: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address.
+
+ Once you've filled out the form, press **Add** to save your changes.
+
+ 
+
+
+ After configuring LDAP auth for your identity, you can authenticate with the identity and obtain an access token using your LDAP credentials.
+
+ ```bash
+ curl --request POST \
+ --url https://app.infisical.com/api/v1/auth/ldap-auth/login \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "identityId": "",
+ "username": "",
+ "password": ""
+ }'
+ ```
+
+
+ For EU Cloud and Self-Hosted users, make sure to replace `https://app.infisical.com` with `https://eu.infisical.com` or your self-hosted instance's URL in the request URL.
+
+
+ If successful, you'll receive an access token in the response body.
+
+ ```json
+ {
+ "accessToken": "your-access-token",
+ "expiresIn": 2592000,
+ "accessTokenMaxTTL": 2592000,
+ "tokenType": "Bearer"
+ }
+ ```
+
+ You can read more about the login API endpoint [here](/api-reference/endpoints/ldap-auth/login).
+
+
+
\ No newline at end of file
diff --git a/docs/documentation/platform/identities/ldap-auth/jumpcloud.mdx b/docs/documentation/platform/identities/ldap-auth/jumpcloud.mdx
new file mode 100644
index 000000000..d51c5e11c
--- /dev/null
+++ b/docs/documentation/platform/identities/ldap-auth/jumpcloud.mdx
@@ -0,0 +1,102 @@
+---
+title: JumpCloud
+description: "Learn how to authenticate with Infisical using LDAP with JumpCloud."
+---
+
+
+
+ LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the Enterprise Tier. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
+
+
+**LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory.
+
+## Guide
+
+
+
+ In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the Manual user entry option.
+ This user will be used as a privileged service account to facilitate Infisical's ability to bind/search the LDAP directory.
+
+ Next after creating the user, under User Security Settings and Permissions > Permission Settings, check the box next to Enable as LDAP Bind DN.
+
+ 
+
+
+
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.
+
+ 
+
+ When creating an identity, you specify an organization level role for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
+
+ 
+
+ Now input a few details for your new identity. Here's some guidance for each field:
+
+ - Name (required): A friendly name for the identity.
+ - Role (required): A role from the Organization Roles tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
+
+ Once you've created an identity, you'll be redirected to a page where you can manage the identity.
+
+
+
+ To configure LDAP auth for your identity, press the **Add Auth Method** button on the identity's page.
+
+ 
+
+ Now select **LDAP Auth** from the list of available auth methods for the identity.
+
+ 
+
+
+ After selecting **LDAP Auth**, you'll see the form you need to fill out to configure LDAP auth for your identity. The following fields are available:
+
+ - `URL`: The LDAP server to connect to (`ldaps://ldap.jumpcloud.com:636`).
+ - `Bind DN`: The distinguished name of object to bind when performing the user search (`uid=,ou=Users,o=,dc=jumpcloud,dc=com`).
+ - `Bind Pass`: The password to use along with Bind DN when performing the user search. This is the password for the user created in the previous step.
+ - `Search Base / DN`: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`).
+ - `User Search Filter`: Template used to construct the LDAP user search filter (`(uid={{username}})`).
+ - `Required Attributes`: A key/value pair of attributes that must be present in the LDAP user entry for them to be authenticated. As an example, if you set key `uid` to value `user1,user2,user3`, then only users with `uid` of `user1`, `user2`, or `user3` will be able to login with this identity. Each value is a comma separated list of attributes.
+ - `CA Certificate`: The CA certificate to use when verifying the LDAP server certificate (instructions to obtain the certificate for JumpCloud [here](https://jumpcloud.com/support/connect-to-ldap-with-tls-ssl)).
+ - `Access Token TTL` _(default is 2592000 equivalent to 30 days)_: The lifetime for an access token in seconds. This value will be referenced at renewal time.
+ - `Access Token Max TTL` _(default is 2592000 equivalent to 30 days)_: The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
+ - `Access Token Max Number of Uses` _(default is 0)_: The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses.
+ - `Access Token Trusted IPs`: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address.
+
+ Once you've filled out the form, press **Add** to save your changes.
+
+ 
+
+
+ After configuring LDAP auth for your identity, you can authenticate with the identity and obtain an access token using your LDAP credentials.
+
+ ```bash
+ curl --request POST \
+ --url https://app.infisical.com/api/v1/auth/ldap-auth/login \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "identityId": "",
+ "username": "",
+ "password": ""
+ }'
+ ```
+
+
+ For EU Cloud and Self-Hosted users, make sure to replace `https://app.infisical.com` with `https://eu.infisical.com` or your self-hosted instance's URL in the request URL.
+
+
+ If successful, you'll receive an access token in the response body.
+
+ ```json
+ {
+ "accessToken": "your-access-token",
+ "expiresIn": 2592000,
+ "accessTokenMaxTTL": 2592000,
+ "tokenType": "Bearer"
+ }
+ ```
+
+ You can read more about the login API endpoint [here](/api-reference/endpoints/ldap-auth/login).
+
+
+
\ No newline at end of file
diff --git a/docs/documentation/platform/identities/oidc-auth/circleci.mdx b/docs/documentation/platform/identities/oidc-auth/circleci.mdx
index ddf74e3fa..6849b77f9 100644
--- a/docs/documentation/platform/identities/oidc-auth/circleci.mdx
+++ b/docs/documentation/platform/identities/oidc-auth/circleci.mdx
@@ -52,7 +52,7 @@ In the following steps, we explore how to create and use identities to access th
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx
index 776d175a4..9a39adba3 100644
--- a/docs/documentation/platform/identities/oidc-auth/general.mdx
+++ b/docs/documentation/platform/identities/oidc-auth/general.mdx
@@ -56,7 +56,7 @@ In the following steps, we explore how to create and use identities to access th
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx
index 47352a339..a377ac37c 100644
--- a/docs/documentation/platform/identities/oidc-auth/github.mdx
+++ b/docs/documentation/platform/identities/oidc-auth/github.mdx
@@ -55,7 +55,7 @@ In the following steps, we explore how to create and use identities to access th
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx
index 228392aa6..b52d2f894 100644
--- a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx
+++ b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx
@@ -55,7 +55,7 @@ In the following steps, we explore how to create and use identities to access th
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/token-auth.mdx b/docs/documentation/platform/identities/token-auth.mdx
index 59c5f9abf..500adf509 100644
--- a/docs/documentation/platform/identities/token-auth.mdx
+++ b/docs/documentation/platform/identities/token-auth.mdx
@@ -38,7 +38,7 @@ using the Token Auth authentication method.
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx
index 4d66e30b4..30f1f10d2 100644
--- a/docs/documentation/platform/identities/universal-auth.mdx
+++ b/docs/documentation/platform/identities/universal-auth.mdx
@@ -42,7 +42,7 @@ using the Universal Auth authentication method.
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/images/platform/identities/ldap/identities-org-add-auth-method-modal.png b/docs/images/platform/identities/ldap/identities-org-add-auth-method-modal.png
new file mode 100644
index 000000000..e9a5f276c
Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-add-auth-method-modal.png differ
diff --git a/docs/images/platform/identities/ldap/identities-org-add-auth-method.png b/docs/images/platform/identities/ldap/identities-org-add-auth-method.png
new file mode 100644
index 000000000..95d301010
Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-add-auth-method.png differ
diff --git a/docs/images/platform/identities/ldap/identities-org-configure-ldap.png b/docs/images/platform/identities/ldap/identities-org-configure-ldap.png
new file mode 100644
index 000000000..c9dfb4950
Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-configure-ldap.png differ
diff --git a/docs/images/platform/identities/ldap/identities-org-create-identity-modal.png b/docs/images/platform/identities/ldap/identities-org-create-identity-modal.png
new file mode 100644
index 000000000..3ac6555e4
Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-create-identity-modal.png differ
diff --git a/docs/images/platform/identities/ldap/identities-org-create-identity.png b/docs/images/platform/identities/ldap/identities-org-create-identity.png
new file mode 100644
index 000000000..1086f6521
Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-create-identity.png differ
diff --git a/docs/images/platform/identities/ldap/jumpcloud-users-management.png b/docs/images/platform/identities/ldap/jumpcloud-users-management.png
new file mode 100644
index 000000000..cc5dc13ca
Binary files /dev/null and b/docs/images/platform/identities/ldap/jumpcloud-users-management.png differ
diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx
index 5ff5d468f..145737e96 100644
--- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx
+++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx
@@ -232,7 +232,7 @@ spec:
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

@@ -407,7 +407,7 @@ spec:
- To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
+ To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

diff --git a/docs/mint.json b/docs/mint.json
index 219c0a69d..82a040f72 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -247,68 +247,88 @@
{
"group": "Authentication Methods",
"pages": [
- "documentation/platform/auth-methods/email-password",
- "documentation/platform/token",
- "documentation/platform/identities/token-auth",
- "documentation/platform/identities/universal-auth",
- "documentation/platform/identities/kubernetes-auth",
- "documentation/platform/identities/gcp-auth",
- "documentation/platform/identities/azure-auth",
- "documentation/platform/identities/aws-auth",
- "documentation/platform/identities/jwt-auth",
{
- "group": "OIDC Auth",
+ "group": "User Authentication",
"pages": [
- "documentation/platform/identities/oidc-auth/general",
- "documentation/platform/identities/oidc-auth/github",
- "documentation/platform/identities/oidc-auth/circleci",
- "documentation/platform/identities/oidc-auth/gitlab",
- "documentation/platform/identities/oidc-auth/terraform-cloud"
- ]
- },
- "documentation/platform/mfa",
- {
- "group": "SSO",
- "pages": [
- "documentation/platform/sso/overview",
- "documentation/platform/sso/google",
- "documentation/platform/sso/github",
- "documentation/platform/sso/gitlab",
- "documentation/platform/sso/okta",
- "documentation/platform/sso/azure",
- "documentation/platform/sso/jumpcloud",
- "documentation/platform/sso/keycloak-saml",
- "documentation/platform/sso/google-saml",
- "documentation/platform/sso/auth0-saml",
+ "documentation/platform/auth-methods/email-password",
{
- "group": "Keycloak OIDC",
+ "group": "SSO",
"pages": [
- "documentation/platform/sso/keycloak-oidc/overview",
- "documentation/platform/sso/keycloak-oidc/group-membership-mapping"
+ "documentation/platform/sso/overview",
+ "documentation/platform/sso/google",
+ "documentation/platform/sso/github",
+ "documentation/platform/sso/gitlab",
+ "documentation/platform/sso/okta",
+ "documentation/platform/sso/azure",
+ "documentation/platform/sso/jumpcloud",
+ "documentation/platform/sso/keycloak-saml",
+ "documentation/platform/sso/google-saml",
+ "documentation/platform/sso/auth0-saml",
+ {
+ "group": "Keycloak OIDC",
+ "pages": [
+ "documentation/platform/sso/keycloak-oidc/overview",
+ "documentation/platform/sso/keycloak-oidc/group-membership-mapping"
+ ]
+ },
+ "documentation/platform/sso/auth0-oidc",
+ "documentation/platform/sso/general-oidc"
]
},
- "documentation/platform/sso/auth0-oidc",
- "documentation/platform/sso/general-oidc"
+ {
+ "group": "LDAP",
+ "pages": [
+ "documentation/platform/ldap/overview",
+ "documentation/platform/ldap/jumpcloud",
+ "documentation/platform/ldap/general"
+ ]
+ },
+ {
+ "group": "SCIM",
+ "pages": [
+ "documentation/platform/scim/overview",
+ "documentation/platform/scim/okta",
+ "documentation/platform/scim/azure",
+ "documentation/platform/scim/jumpcloud",
+ "documentation/platform/scim/group-mappings"
+ ]
+ }
]
},
+
{
- "group": "LDAP",
+ "group": "Machine Identities",
"pages": [
- "documentation/platform/ldap/overview",
- "documentation/platform/ldap/jumpcloud",
- "documentation/platform/ldap/general"
- ]
- },
- {
- "group": "SCIM",
- "pages": [
- "documentation/platform/scim/overview",
- "documentation/platform/scim/okta",
- "documentation/platform/scim/azure",
- "documentation/platform/scim/jumpcloud",
- "documentation/platform/scim/group-mappings"
+ "documentation/platform/identities/token-auth",
+ "documentation/platform/identities/universal-auth",
+ "documentation/platform/identities/kubernetes-auth",
+ "documentation/platform/identities/gcp-auth",
+ "documentation/platform/identities/azure-auth",
+ "documentation/platform/identities/aws-auth",
+ "documentation/platform/identities/jwt-auth",
+
+ {
+ "group": "OIDC Auth",
+ "pages": [
+ "documentation/platform/identities/oidc-auth/general",
+ "documentation/platform/identities/oidc-auth/github",
+ "documentation/platform/identities/oidc-auth/circleci",
+ "documentation/platform/identities/oidc-auth/gitlab",
+ "documentation/platform/identities/oidc-auth/terraform-cloud"
+ ]
+ },
+
+ {
+ "group": "LDAP Auth",
+ "pages": [
+ "documentation/platform/identities/ldap-auth/general",
+ "documentation/platform/identities/ldap-auth/jumpcloud"
+ ]
+ }
]
},
+ "documentation/platform/token",
+ "documentation/platform/mfa",
"documentation/platform/github-org-sync"
]
},
@@ -717,6 +737,16 @@
"api-reference/endpoints/jwt-auth/revoke"
]
},
+ {
+ "group": "LDAP Auth",
+ "pages": [
+ "api-reference/endpoints/ldap-auth/login",
+ "api-reference/endpoints/ldap-auth/attach",
+ "api-reference/endpoints/ldap-auth/retrieve",
+ "api-reference/endpoints/ldap-auth/update",
+ "api-reference/endpoints/ldap-auth/revoke"
+ ]
+ },
{
"group": "Groups",
"pages": [
diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx
index 36daed4b7..f726566cd 100644
--- a/frontend/src/hooks/api/auditLogs/constants.tsx
+++ b/frontend/src/hooks/api/auditLogs/constants.tsx
@@ -182,10 +182,17 @@ export const eventToNameMap: { [K in EventType]: string } = {
"Microsoft Teams Workflow Integration Check Installation Status",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration",
- [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration"
+ [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]:
+ "List Microsoft Teams Workflow Integration",
+
+ [EventType.LOGIN_IDENTITY_LDAP_AUTH]: "Identity login via LDAP Auth",
+ [EventType.ADD_IDENTITY_LDAP_AUTH]: "Attached LDAP Auth to identity",
+ [EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
+ [EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
+ [EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity"
};
-export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
[UserAgentType.WEB]: "Web",
[UserAgentType.CLI]: "CLI",
[UserAgentType.K8_OPERATOR]: "K8s operator",
diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx
index ac57def4f..b74969d6d 100644
--- a/frontend/src/hooks/api/auditLogs/enums.tsx
+++ b/frontend/src/hooks/api/auditLogs/enums.tsx
@@ -47,6 +47,13 @@ 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",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
+
+ LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
+ ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",
+ UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
+ GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
+ REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
+
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx
index c11d7dc11..97acd6dfc 100644
--- a/frontend/src/hooks/api/identities/constants.tsx
+++ b/frontend/src/hooks/api/identities/constants.tsx
@@ -8,5 +8,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
+ [IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
};
diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx
index 415492e00..8a8d99fae 100644
--- a/frontend/src/hooks/api/identities/enums.tsx
+++ b/frontend/src/hooks/api/identities/enums.tsx
@@ -6,6 +6,7 @@ export enum IdentityAuthMethod {
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth",
+ LDAP_AUTH = "ldap-auth",
JWT_AUTH = "jwt-auth"
}
diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx
index f3b9fa012..bf49387ac 100644
--- a/frontend/src/hooks/api/identities/index.tsx
+++ b/frontend/src/hooks/api/identities/index.tsx
@@ -1,51 +1,4 @@
export { identityAuthToNameMap } from "./constants";
export { IdentityAuthMethod } from "./enums";
-export {
- useAddIdentityAwsAuth,
- useAddIdentityAzureAuth,
- useAddIdentityGcpAuth,
- useAddIdentityJwtAuth,
- useAddIdentityKubernetesAuth,
- useAddIdentityOidcAuth,
- useAddIdentityTokenAuth,
- useAddIdentityUniversalAuth,
- useCreateIdentity,
- useCreateIdentityUniversalAuthClientSecret,
- useCreateTokenIdentityTokenAuth,
- useDeleteIdentity,
- useDeleteIdentityAwsAuth,
- useDeleteIdentityAzureAuth,
- useDeleteIdentityGcpAuth,
- useDeleteIdentityJwtAuth,
- useDeleteIdentityKubernetesAuth,
- useDeleteIdentityOidcAuth,
- useDeleteIdentityTokenAuth,
- useDeleteIdentityUniversalAuth,
- useRevokeIdentityTokenAuthToken,
- useRevokeIdentityUniversalAuthClientSecret,
- useUpdateIdentity,
- useUpdateIdentityAwsAuth,
- useUpdateIdentityAzureAuth,
- useUpdateIdentityGcpAuth,
- useUpdateIdentityJwtAuth,
- useUpdateIdentityKubernetesAuth,
- useUpdateIdentityOidcAuth,
- useUpdateIdentityTokenAuth,
- useUpdateIdentityTokenAuthToken,
- useUpdateIdentityUniversalAuth
-} from "./mutations";
-export {
- useGetIdentityAwsAuth,
- useGetIdentityAzureAuth,
- useGetIdentityById,
- useGetIdentityGcpAuth,
- useGetIdentityJwtAuth,
- useGetIdentityKubernetesAuth,
- useGetIdentityOidcAuth,
- useGetIdentityProjectMemberships,
- useGetIdentityTokenAuth,
- useGetIdentityTokensTokenAuth,
- useGetIdentityUniversalAuth,
- useGetIdentityUniversalAuthClientSecrets,
- useSearchIdentities
-} from "./queries";
+export * from "./mutations";
+export * from "./queries";
diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx
index d68595ad5..e0077527f 100644
--- a/frontend/src/hooks/api/identities/mutations.tsx
+++ b/frontend/src/hooks/api/identities/mutations.tsx
@@ -10,6 +10,7 @@ import {
AddIdentityGcpAuthDTO,
AddIdentityJwtAuthDTO,
AddIdentityKubernetesAuthDTO,
+ AddIdentityLdapAuthDTO,
AddIdentityOidcAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
@@ -25,6 +26,7 @@ import {
DeleteIdentityGcpAuthDTO,
DeleteIdentityJwtAuthDTO,
DeleteIdentityKubernetesAuthDTO,
+ DeleteIdentityLdapAuthDTO,
DeleteIdentityOidcAuthDTO,
DeleteIdentityTokenAuthDTO,
DeleteIdentityUniversalAuthClientSecretDTO,
@@ -36,6 +38,7 @@ import {
IdentityGcpAuth,
IdentityJwtAuth,
IdentityKubernetesAuth,
+ IdentityLdapAuth,
IdentityOidcAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
@@ -47,6 +50,7 @@ import {
UpdateIdentityGcpAuthDTO,
UpdateIdentityJwtAuthDTO,
UpdateIdentityKubernetesAuthDTO,
+ UpdateIdentityLdapAuthDTO,
UpdateIdentityOidcAuthDTO,
UpdateIdentityTokenAuthDTO,
UpdateIdentityUniversalAuthDTO,
@@ -1049,3 +1053,116 @@ export const useRevokeIdentityTokenAuthToken = () => {
}
});
};
+
+export const useAddIdentityLdapAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
+ `/api/v1/auth/ldap-auth/identities/${identityId}`,
+ {
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+ return data.identityLdapAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
+ });
+ }
+ });
+};
+
+export const useUpdateIdentityLdapAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
+ `/api/v1/auth/ldap-auth/identities/${identityId}`,
+ {
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+ return data.identityLdapAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
+ });
+ }
+ });
+};
+
+export const useDeleteIdentityLdapAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const { data } = await apiRequest.delete(`/api/v1/auth/ldap-auth/identities/${identityId}`);
+ return data.identityLdapAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
+ });
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx
index 6b8a1d1cc..3bc94534c 100644
--- a/frontend/src/hooks/api/identities/queries.tsx
+++ b/frontend/src/hooks/api/identities/queries.tsx
@@ -11,6 +11,7 @@ import {
IdentityGcpAuth,
IdentityJwtAuth,
IdentityKubernetesAuth,
+ IdentityLdapAuth,
IdentityMembership,
IdentityMembershipOrg,
IdentityOidcAuth,
@@ -34,6 +35,7 @@ export const identitiesKeys = {
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const,
+ getIdentityLdapAuth: (identityId: string) => [{ identityId }, "identity-ldap-auth"] as const,
getIdentityTokensTokenAuth: (identityId: string) =>
[{ identityId }, "identity-tokens-token-auth"] as const,
getIdentityProjectMemberships: (identityId: string) =>
@@ -231,6 +233,27 @@ export const useGetIdentityTokenAuth = (
});
};
+export const useGetIdentityLdapAuth = (
+ identityId: string,
+ options?: TReactQueryOptions["options"]
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityLdapAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityLdapAuth }
+ } = await apiRequest.get<{ identityLdapAuth: IdentityLdapAuth }>(
+ `/api/v1/auth/ldap-auth/identities/${identityId}`
+ );
+ return identityLdapAuth;
+ },
+ staleTime: 0,
+ gcTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
export const useGetIdentityTokensTokenAuth = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts
index ca06219aa..c5f8cbc4a 100644
--- a/frontend/src/hooks/api/identities/types.ts
+++ b/frontend/src/hooks/api/identities/types.ts
@@ -425,6 +425,72 @@ export type IdentityTokenAuth = {
accessTokenTrustedIps: IdentityTrustedIp[];
};
+export type AddIdentityLdapAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ url: string;
+ bindDN: string;
+ bindPass: string;
+ searchBase: string;
+ searchFilter: string;
+ ldapCaCertificate?: string;
+ allowedFields?: {
+ key: string;
+ value: string;
+ }[];
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityLdapAuthDTO = {
+ identityId: string;
+ organizationId: string;
+ url?: string;
+ bindDN?: string;
+ bindPass?: string;
+ searchBase?: string;
+ searchFilter?: string;
+ ldapCaCertificate?: string;
+ allowedFields?: {
+ key: string;
+ value: string;
+ }[];
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityLdapAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type IdentityLdapAuth = {
+ url: string;
+ bindDN: string;
+ bindPass: string;
+ searchBase: string;
+ searchFilter: string;
+ ldapCaCertificate?: string;
+ allowedFields?: {
+ key: string;
+ value: string;
+ }[];
+
+ identityId: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
export type AddIdentityTokenAuthDTO = {
organizationId: string;
identityId: string;
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
index 1380ebb39..0444b3bd1 100644
--- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
@@ -13,6 +13,7 @@ import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityJwtAuthForm } from "./IdentityJwtAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
+import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
@@ -46,6 +47,7 @@ const identityAuthMethods = [
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
+ { label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
{
label: "JWT Auth",
value: IdentityAuthMethod.JWT_AUTH
@@ -186,6 +188,16 @@ export const IdentityAuthMethodModalContent = ({
handlePopUpToggle={handlePopUpToggle}
/>
)
+ },
+
+ [IdentityAuthMethod.LDAP_AUTH]: {
+ render: () => (
+
+ )
}
};
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx
new file mode 100644
index 000000000..62080bfd1
--- /dev/null
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx
@@ -0,0 +1,608 @@
+import { useEffect, useState } from "react";
+import { Controller, useFieldArray, useForm } from "react-hook-form";
+import { faPlus, faQuestionCircle, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ FormControl,
+ IconButton,
+ Input,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+ TextArea,
+ Tooltip
+} from "@app/components/v2";
+import { useOrganization, useSubscription } from "@app/context";
+import {
+ useAddIdentityLdapAuth,
+ useGetIdentityLdapAuth,
+ useUpdateIdentityLdapAuth
+} from "@app/hooks/api";
+import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+import { IdentityFormTab } from "./types";
+
+const schema = z
+ .object({
+ url: z.string().min(1),
+ bindDN: z.string(),
+ bindPass: z.string(),
+ searchBase: z.string(),
+ searchFilter: z.string(), // defaults to (uid={{username}})
+ ldapCaCertificate: z
+ .string()
+ .optional()
+ .transform((val) => val || undefined),
+ allowedFields: z
+ .object({
+ key: z.string().trim(),
+ value: z
+ .string()
+ .trim()
+ .transform((val) => val.replace(/\s/g, ""))
+ })
+ .array()
+ .optional(),
+
+ accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
+ message: "Access Token TTL cannot be greater than 315360000"
+ }),
+ accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
+ message: "Access Token Max TTL cannot be greater than 315360000"
+ }),
+ accessTokenNumUsesLimit: z.string(),
+ accessTokenTrustedIps: z
+ .array(
+ z.object({
+ ipAddress: z.string().max(50)
+ })
+ )
+ .min(1)
+ })
+ .required();
+
+export type FormData = z.infer;
+
+type Props = {
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
+ handlePopUpToggle: (
+ popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
+ state?: boolean
+ ) => void;
+ identityId?: string;
+ isUpdate?: boolean;
+};
+
+export const IdentityLdapAuthForm = ({
+ handlePopUpOpen,
+ handlePopUpToggle,
+ identityId,
+ isUpdate
+}: Props) => {
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+ const { subscription } = useSubscription();
+
+ const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth();
+ const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
+ const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration);
+
+ const { data } = useGetIdentityLdapAuth(identityId ?? "", {
+ enabled: isUpdate
+ });
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ url: "",
+ bindDN: "",
+ bindPass: "",
+ searchBase: "",
+ searchFilter: "(uid={{username}})",
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ }
+ });
+
+ const {
+ fields: accessTokenTrustedIpsFields,
+ append: appendAccessTokenTrustedIp,
+ remove: removeAccessTokenTrustedIp
+ } = useFieldArray({ control, name: "accessTokenTrustedIps" });
+
+ const {
+ fields: allowedFieldsFields,
+ append: appendAllowedField,
+ remove: removeAllowedField
+ } = useFieldArray({ control, name: "allowedFields" });
+
+ useEffect(() => {
+ if (data) {
+ reset({
+ url: data.url,
+ bindDN: data.bindDN,
+ bindPass: data.bindPass,
+ searchBase: data.searchBase,
+ searchFilter: data.searchFilter,
+ ldapCaCertificate: data.ldapCaCertificate || undefined,
+ allowedFields: data.allowedFields,
+ accessTokenTTL: String(data.accessTokenTTL),
+ accessTokenMaxTTL: String(data.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
+ accessTokenTrustedIps: data.accessTokenTrustedIps.map(
+ ({ ipAddress, prefix }: IdentityTrustedIp) => {
+ return {
+ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
+ };
+ }
+ )
+ });
+ } else {
+ reset({
+ url: "",
+ bindDN: "",
+ bindPass: "",
+ searchBase: "",
+ searchFilter: "(uid={{username}})",
+ ldapCaCertificate: undefined,
+ allowedFields: [],
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ });
+ }
+ }, [data]);
+
+ useEffect(() => {
+ if (!subscription?.ldap) {
+ handlePopUpOpen("upgradePlan");
+ handlePopUpToggle("identityAuthMethod", false);
+ }
+ }, [subscription]);
+
+ const onFormSubmit = async ({
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }: FormData) => {
+ try {
+ if (!identityId) return;
+
+ if (data) {
+ await updateMutateAsync({
+ organizationId: orgId,
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ } else {
+ await addMutateAsync({
+ organizationId: orgId,
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ }
+
+ handlePopUpToggle("identityAuthMethod", false);
+
+ createNotification({
+ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
+ type: "success"
+ });
+
+ reset();
+ } catch {
+ createNotification({
+ text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/organization/AccessManagementPage/route.tsx b/frontend/src/pages/organization/AccessManagementPage/route.tsx
index 1401f57c1..68e6a7742 100644
--- a/frontend/src/pages/organization/AccessManagementPage/route.tsx
+++ b/frontend/src/pages/organization/AccessManagementPage/route.tsx
@@ -30,7 +30,7 @@ export const Route = createFileRoute(
link: linkOptions({ to: "/" })
},
{
- label: "access control"
+ label: "Access Control"
}
]
})
diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx
index 59440c814..b015e0679 100644
--- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx
+++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx
@@ -38,7 +38,7 @@ import { useGetUserWorkspaces } from "@app/hooks/api";
import {
eventToNameMap,
secretEvents,
- userAgentTTypeoNameMap
+ userAgentTypeToNameMap
} from "@app/hooks/api/auditLogs/constants";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
@@ -47,7 +47,7 @@ import { LogFilterItem } from "./LogFilterItem";
import { AuditLogFilterFormData, Presets } from "./types";
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
-const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
+const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({
label,
value
}));
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
index 84082272c..62039ceb9 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
@@ -11,6 +11,7 @@ import {
useDeleteIdentityGcpAuth,
useDeleteIdentityJwtAuth,
useDeleteIdentityKubernetesAuth,
+ useDeleteIdentityLdapAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
@@ -22,6 +23,7 @@ import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent";
import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent";
import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent";
import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent";
+import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
@@ -61,6 +63,7 @@ export const Content = ({
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
+ const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth();
let Component: (props: ViewAuthMethodProps) => JSX.Element;
let revokeMethod: (revokeOptions: TRevokeOptions) => Promise;
@@ -100,6 +103,10 @@ export const Content = ({
revokeMethod = revokeJwtAuth;
Component = ViewIdentityJwtAuthContent;
break;
+ case IdentityAuthMethod.LDAP_AUTH:
+ revokeMethod = revokeLdapAuth;
+ Component = ViewIdentityLdapAuthContent;
+ break;
default:
throw new Error(`Unhandled Auth Method: ${authMethod}`);
}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx
index 64eea9cf6..e7130d8a2 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx
@@ -47,10 +47,10 @@ export const ViewIdentityAwsAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx
index fa146e4ca..ea9ee09ea 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx
@@ -47,10 +47,10 @@ export const ViewIdentityAzureAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx
index 9eefe96fd..4008850e9 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx
@@ -47,10 +47,10 @@ export const ViewIdentityGcpAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx
index d16635937..b0e50278e 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx
@@ -49,10 +49,10 @@ export const ViewIdentityJwtAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx
index da0455160..214bb1b21 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx
@@ -51,10 +51,10 @@ export const ViewIdentityKubernetesAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx
new file mode 100644
index 000000000..f732fd5ed
--- /dev/null
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx
@@ -0,0 +1,103 @@
+import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
+import { useGetIdentityLdapAuth } from "@app/hooks/api";
+import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
+import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
+
+import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
+import { ViewAuthMethodProps } from "./types";
+
+export const ViewIdentityLdapAuthContent = ({
+ identityId,
+ handlePopUpToggle,
+ handlePopUpOpen,
+ onDelete,
+ popUp
+}: ViewAuthMethodProps) => {
+ const { data, isPending } = useGetIdentityLdapAuth(identityId);
+
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+ );
+ }
+
+ if (popUp.identityAuthMethod.isOpen) {
+ return (
+
+ );
+ }
+
+ return (
+ handlePopUpOpen("identityAuthMethod")}
+ onDelete={onDelete}
+ >
+
+ {data.accessTokenTTL}
+
+
+ {data.accessTokenMaxTTL}
+
+
+ {data.accessTokenNumUsesLimit}
+
+
+ {data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
+
+ {data.url}
+ {data.bindDN}
+
+ {data.bindPass}
}
+ >
+
+
+
+ Reveal
+
+
+
+
+
+ {data.searchBase}
+
+ {data.searchFilter}
+
+ {data.ldapCaCertificate && (
+ {data.ldapCaCertificate}
+ }
+ >
+
+
+
+ Reveal
+
+
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx
index 116ecfd38..a326fda45 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx
@@ -48,10 +48,10 @@ export const ViewIdentityOidcAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx
index 0e28516fa..516289f74 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx
@@ -50,10 +50,10 @@ export const ViewIdentityTokenAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx
index ca1c30eed..3a794ce60 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx
@@ -62,10 +62,10 @@ export const ViewIdentityUniversalAuthContent = ({
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
-
+
{data.accessTokenTTL}
-
+
{data.accessTokenMaxTTL}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx
index 20edc25c1..fd4a86260 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx
@@ -20,7 +20,7 @@ export const Route = createFileRoute(
link: linkOptions({ to: "/organization/access-management" })
},
{
- label: "identities"
+ label: "Identities"
}
]
})