diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 6ec542c6b..74571e2d0 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..db7ba5281
--- /dev/null
+++ b/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts
@@ -0,0 +1,40 @@
+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.string("uniqueAttribute").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..e843648bc
--- /dev/null
+++ b/backend/src/db/schemas/identity-ldap-auths.ts
@@ -0,0 +1,33 @@
+// 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(),
+ uniqueAttribute: 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 7fd77da6c..b7580a110 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",
@@ -227,7 +228,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..92f4d54a7 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..ab23bdb45 100644
--- a/backend/src/ee/services/ldap-config/ldap-fns.ts
+++ b/backend/src/ee/services/ldap-config/ldap-fns.ts
@@ -2,7 +2,7 @@ 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 {
@@ -20,7 +20,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 ae6bbbcab..62ba2c974 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",
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 03e23a69d..87b043451 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";
@@ -353,6 +355,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);
@@ -1438,6 +1441,16 @@ export const registerRoutes = async (
kmsService
});
+ const identityLdapAuthService = identityLdapAuthServiceFactory({
+ identityLdapAuthDAL,
+ permissionService,
+ kmsService,
+ identityAccessTokenDAL,
+ identityOrgMembershipDAL,
+ licenseService,
+ identityDAL
+ });
+
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
@@ -1698,6 +1711,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..bfc195f0a
--- /dev/null
+++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts
@@ -0,0 +1,465 @@
+/* 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 { ApiDocsTags } 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: "",
+ 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(),
+ username: z.string(),
+ password: z.string()
+ }),
+ 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 ||
+ !req.passportMachineIdentity.user.mail ||
+ !req.passportMachineIdentity.user.uid
+ ) {
+ 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()
+ }),
+ body: z
+ .object({
+ url: z.string().trim().min(1),
+ bindDN: z.string().trim().min(1),
+ bindPass: z.string().trim().min(1),
+ searchBase: z.string().trim().min(1),
+ uniqueAttribute: z.string().trim().min(1).default("uidNumber"),
+ searchFilter: z.string().trim().min(1).default("(uid={{username}})"),
+ allowedFields: AllowedFieldsSchema.array().optional(),
+ ldapCaCertificate: z.string().trim().optional(),
+
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
+ accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000),
+ accessTokenMaxTTL: z.number().int().min(1).max(315360000).default(2592000),
+ accessTokenNumUsesLimit: z.number().int().min(0).default(0)
+ })
+ .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()
+ }),
+ body: z
+ .object({
+ url: z.string().trim().min(1),
+ bindDN: z.string().trim().min(1),
+ bindPass: z.string().trim().min(1),
+ searchBase: z.string().trim().min(1),
+ uniqueAttribute: z.string().trim().min(1).default("uidNumber"),
+ searchFilter: z.string().trim().min(1).default("(uid={{username}})"),
+ allowedFields: AllowedFieldsSchema.array().optional(),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .optional(),
+ accessTokenTTL: z.number().int().min(0).max(315360000).optional(),
+ accessTokenNumUsesLimit: z.number().int().min(0).optional(),
+ accessTokenMaxTTL: z.number().int().max(315360000).min(0).optional()
+ })
+ .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()
+ }),
+ 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()
+ }),
+ 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-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..6b55d1917
--- /dev/null
+++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts
@@ -0,0 +1,547 @@
+/* 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,
+ uniqueUserAttribute: ldapAuth.uniqueAttribute,
+ searchBase: ldapAuth.searchBase,
+ searchFilter: ldapAuth.searchFilter,
+ caCert: ldapCaCertificate || "",
+ allowedFields: parsedAllowedFields
+ };
+
+ const opts = {
+ server: {
+ url: ldapAuth.url,
+ bindDN,
+ bindCredentials: bindPass,
+ uniqueUserAttribute: ldapAuth.uniqueAttribute,
+ searchBase: ldapAuth.searchBase,
+ searchFilter: ldapAuth.searchFilter || "(uid={{username}})",
+ ...(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,
+ uniqueAttribute,
+ 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);
+ });
+
+ 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)
+ });
+
+ if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
+
+ 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,
+ uniqueAttribute,
+ 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,
+ uniqueAttribute,
+ 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);
+ });
+
+ 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,
+ uniqueAttribute,
+ 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..cba6acbcb
--- /dev/null
+++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts
@@ -0,0 +1,58 @@
+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;
+ uniqueAttribute: 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;
+ uniqueAttribute?: 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/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx
index 36daed4b7..d48d8ae80 100644
--- a/frontend/src/hooks/api/auditLogs/constants.tsx
+++ b/frontend/src/hooks/api/auditLogs/constants.tsx
@@ -182,7 +182,14 @@ 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 } = {
diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx
index ac57def4f..59ae35d45 100644
--- a/frontend/src/hooks/api/auditLogs/enums.tsx
+++ b/frontend/src/hooks/api/auditLogs/enums.tsx
@@ -176,5 +176,11 @@ export enum EventType {
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
- MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list"
+ MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
+
+ 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"
}
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..7d9b4fbcb 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,117 @@ export const useRevokeIdentityTokenAuthToken = () => {
}
});
};
+
+export const useAddIdentityLdapAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ uniqueAttribute,
+ 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,
+ uniqueAttribute,
+ 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,
+ uniqueAttribute,
+ 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,
+ uniqueAttribute,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+ return data.identityLdapAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(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..74ac4b214 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,26 @@ 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..0ad6e9316 100644
--- a/frontend/src/hooks/api/identities/types.ts
+++ b/frontend/src/hooks/api/identities/types.ts
@@ -425,6 +425,75 @@ export type IdentityTokenAuth = {
accessTokenTrustedIps: IdentityTrustedIp[];
};
+export type AddIdentityLdapAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ url: string;
+ bindDN: string;
+ bindPass: string;
+ searchBase: string;
+ searchFilter: string;
+ uniqueAttribute: 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;
+ uniqueAttribute?: 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;
+ uniqueAttribute: 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..8da222295
--- /dev/null
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx
@@ -0,0 +1,630 @@
+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(),
+ uniqueAttribute: z.string(), // defaults to uidNumber
+ 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: "",
+ uniqueAttribute: "uidNumber",
+ 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,
+ uniqueAttribute: data.uniqueAttribute,
+ 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: "",
+ uniqueAttribute: "uidNumber",
+ 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,
+ uniqueAttribute,
+ searchFilter,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }: FormData) => {
+ try {
+ if (!identityId) return;
+
+ if (data) {
+ await updateMutateAsync({
+ organizationId: orgId,
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ uniqueAttribute,
+ ldapCaCertificate,
+ allowedFields,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ } else {
+ await addMutateAsync({
+ organizationId: orgId,
+ identityId,
+ url,
+ bindDN,
+ bindPass,
+ searchBase,
+ searchFilter,
+ uniqueAttribute,
+ 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/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/ViewIdentityLdapAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx
new file mode 100644
index 000000000..ae6200d8a
--- /dev/null
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx
@@ -0,0 +1,106 @@
+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.uniqueAttribute}
+
+ {data.searchFilter}
+
+ {data.ldapCaCertificate && (
+ {data.ldapCaCertificate}
+ }
+ >
+
+
+
+ Reveal
+
+
+
+ )}
+
+
+ );
+};
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"
}
]
})