mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 16:26:37 +00:00
Merge pull request #1729 from Infisical/groups-phase-3
Groups Phase 3 (LDAP)
This commit is contained in:
Generated
+108
-9
@@ -45,6 +45,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
|
"ldapjs": "^3.0.7",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
@@ -2510,6 +2511,83 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ldapjs/asn1": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA=="
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/attribute": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"process-warning": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/change": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"@ldapjs/attribute": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/controls": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "^1.2.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw=="
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/dn": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"process-warning": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/filter": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"process-warning": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/messages": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "^2.0.0",
|
||||||
|
"@ldapjs/attribute": "^1.0.0",
|
||||||
|
"@ldapjs/change": "^1.0.0",
|
||||||
|
"@ldapjs/controls": "^2.1.0",
|
||||||
|
"@ldapjs/dn": "^1.1.0",
|
||||||
|
"@ldapjs/filter": "^2.1.1",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"process-warning": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/protocol": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
||||||
|
},
|
||||||
"node_modules/@lukeed/ms": {
|
"node_modules/@lukeed/ms": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
|
||||||
@@ -9304,15 +9382,7 @@
|
|||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ldapauth-fork/node_modules/lru-cache": {
|
"node_modules/ldapauth-fork/node_modules/ldapjs": {
|
||||||
"version": "7.18.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
|
||||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ldapjs": {
|
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
|
||||||
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
|
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
|
||||||
@@ -9330,6 +9400,35 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ldapauth-fork/node_modules/lru-cache": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ldapjs": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "^2.0.0",
|
||||||
|
"@ldapjs/attribute": "^1.0.0",
|
||||||
|
"@ldapjs/change": "^1.0.0",
|
||||||
|
"@ldapjs/controls": "^2.1.0",
|
||||||
|
"@ldapjs/dn": "^1.1.0",
|
||||||
|
"@ldapjs/filter": "^2.1.1",
|
||||||
|
"@ldapjs/messages": "^1.3.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"abstract-logging": "^2.0.1",
|
||||||
|
"assert-plus": "^1.0.0",
|
||||||
|
"backoff": "^2.5.0",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"vasync": "^2.2.1",
|
||||||
|
"verror": "^1.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
|
"ldapjs": "^3.0.7",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
|
|||||||
Vendored
+4
@@ -74,6 +74,9 @@ import {
|
|||||||
TLdapConfigs,
|
TLdapConfigs,
|
||||||
TLdapConfigsInsert,
|
TLdapConfigsInsert,
|
||||||
TLdapConfigsUpdate,
|
TLdapConfigsUpdate,
|
||||||
|
TLdapGroupMaps,
|
||||||
|
TLdapGroupMapsInsert,
|
||||||
|
TLdapGroupMapsUpdate,
|
||||||
TOrganizations,
|
TOrganizations,
|
||||||
TOrganizationsInsert,
|
TOrganizationsInsert,
|
||||||
TOrganizationsUpdate,
|
TOrganizationsUpdate,
|
||||||
@@ -398,6 +401,7 @@ declare module "knex/types/tables" {
|
|||||||
>;
|
>;
|
||||||
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
||||||
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
||||||
|
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
||||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||||
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
||||||
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.LdapGroupMap))) {
|
||||||
|
await knex.schema.createTable(TableName.LdapGroupMap, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("ldapConfigId").notNullable();
|
||||||
|
t.foreign("ldapConfigId").references("id").inTable(TableName.LdapConfig).onDelete("CASCADE");
|
||||||
|
t.string("ldapGroupCN").notNullable();
|
||||||
|
t.uuid("groupId").notNullable();
|
||||||
|
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
t.unique(["ldapGroupCN", "groupId", "ldapConfigId"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.LdapGroupMap);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||||
|
t.string("groupSearchBase").notNullable().defaultTo("");
|
||||||
|
t.string("groupSearchFilter").notNullable().defaultTo("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.LdapGroupMap);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.LdapGroupMap);
|
||||||
|
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||||
|
t.dropColumn("groupSearchBase");
|
||||||
|
t.dropColumn("groupSearchFilter");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ export * from "./incident-contacts";
|
|||||||
export * from "./integration-auths";
|
export * from "./integration-auths";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./ldap-configs";
|
export * from "./ldap-configs";
|
||||||
|
export * from "./ldap-group-maps";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./org-bots";
|
export * from "./org-bots";
|
||||||
export * from "./org-memberships";
|
export * from "./org-memberships";
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export const LdapConfigsSchema = z.object({
|
|||||||
caCertIV: z.string(),
|
caCertIV: z.string(),
|
||||||
caCertTag: z.string(),
|
caCertTag: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
groupSearchBase: z.string().default(""),
|
||||||
|
groupSearchFilter: z.string().default("")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;
|
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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 { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const LdapGroupMapsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
ldapConfigId: z.string().uuid(),
|
||||||
|
ldapGroupCN: z.string(),
|
||||||
|
groupId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TLdapGroupMaps = z.infer<typeof LdapGroupMapsSchema>;
|
||||||
|
export type TLdapGroupMapsInsert = Omit<z.input<typeof LdapGroupMapsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TLdapGroupMapsUpdate = Partial<Omit<z.input<typeof LdapGroupMapsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -60,6 +60,7 @@ export enum TableName {
|
|||||||
SecretRotationOutput = "secret_rotation_outputs",
|
SecretRotationOutput = "secret_rotation_outputs",
|
||||||
SamlConfig = "saml_configs",
|
SamlConfig = "saml_configs",
|
||||||
LdapConfig = "ldap_configs",
|
LdapConfig = "ldap_configs",
|
||||||
|
LdapGroupMap = "ldap_group_maps",
|
||||||
AuditLog = "audit_logs",
|
AuditLog = "audit_logs",
|
||||||
GitAppInstallSession = "git_app_install_sessions",
|
GitAppInstallSession = "git_app_install_sessions",
|
||||||
GitAppOrg = "git_app_org",
|
GitAppOrg = "git_app_org",
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import { FastifyRequest } from "fastify";
|
|||||||
import LdapStrategy from "passport-ldapauth";
|
import LdapStrategy from "passport-ldapauth";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { LdapConfigsSchema } from "@app/db/schemas";
|
import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
|
||||||
|
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
|
||||||
|
import { searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -50,20 +52,33 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req: IncomingMessage, user, cb) => {
|
async (req: IncomingMessage, user, cb) => {
|
||||||
try {
|
try {
|
||||||
|
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
|
||||||
|
|
||||||
|
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||||
|
const searchFilter =
|
||||||
|
ldapConfig.groupSearchFilter ||
|
||||||
|
groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn);
|
||||||
|
|
||||||
|
const shouldProcessGroups = ldapConfig.groupSearchFilter && ldapConfig.groupSearchBase;
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
|
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
|
||||||
|
ldapConfigId: ldapConfig.id,
|
||||||
externalId: user.uidNumber,
|
externalId: user.uidNumber,
|
||||||
username: user.uid,
|
username: user.uid,
|
||||||
firstName: user.givenName,
|
firstName: user.givenName ?? user.cn ?? "",
|
||||||
lastName: user.sn,
|
lastName: user.sn ?? "",
|
||||||
emails: user.mail ? [user.mail] : [],
|
emails: user.mail ? [user.mail] : [],
|
||||||
|
groups: shouldProcessGroups
|
||||||
|
? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase)
|
||||||
|
: undefined,
|
||||||
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
||||||
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb(null, { isUserCompleted, providerAuthToken });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error(err);
|
logger.error(error);
|
||||||
return cb(err, false);
|
return cb(error, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -117,6 +132,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
bindDN: z.string(),
|
bindDN: z.string(),
|
||||||
bindPass: z.string(),
|
bindPass: z.string(),
|
||||||
searchBase: z.string(),
|
searchBase: z.string(),
|
||||||
|
groupSearchBase: z.string(),
|
||||||
|
groupSearchFilter: z.string(),
|
||||||
caCert: z.string()
|
caCert: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -148,6 +165,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
bindDN: z.string().trim(),
|
bindDN: z.string().trim(),
|
||||||
bindPass: z.string().trim(),
|
bindPass: z.string().trim(),
|
||||||
searchBase: z.string().trim(),
|
searchBase: z.string().trim(),
|
||||||
|
groupSearchBase: z.string().trim().default(""),
|
||||||
|
groupSearchFilter: z.string().trim().default(""),
|
||||||
caCert: z.string().trim().default("")
|
caCert: z.string().trim().default("")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -183,6 +202,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
bindDN: z.string().trim(),
|
bindDN: z.string().trim(),
|
||||||
bindPass: z.string().trim(),
|
bindPass: z.string().trim(),
|
||||||
searchBase: z.string().trim(),
|
searchBase: z.string().trim(),
|
||||||
|
groupSearchBase: z.string().trim(),
|
||||||
|
groupSearchFilter: z.string().trim(),
|
||||||
caCert: z.string().trim()
|
caCert: z.string().trim()
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
@@ -204,4 +225,106 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
return ldap;
|
return ldap;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/config/:configId/group-maps",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
configId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
ldapConfigId: z.string(),
|
||||||
|
ldapGroupCN: z.string(),
|
||||||
|
group: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ldapGroupMaps = await server.services.ldap.getLdapGroupMaps({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
ldapConfigId: req.params.configId
|
||||||
|
});
|
||||||
|
return ldapGroupMaps;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/config/:configId/group-maps",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
configId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
ldapGroupCN: z.string().trim(),
|
||||||
|
groupSlug: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: LdapGroupMapsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ldapGroupMap = await server.services.ldap.createLdapGroupMap({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
ldapConfigId: req.params.configId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return ldapGroupMap;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/config/:configId/group-maps/:groupMapId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
configId: z.string().trim(),
|
||||||
|
groupMapId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: LdapGroupMapsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ldapGroupMap = await server.services.ldap.deleteLdapGroupMap({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
ldapConfigId: req.params.configId,
|
||||||
|
ldapGroupMapId: req.params.groupMapId
|
||||||
|
});
|
||||||
|
return ldapGroupMap;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ const addAcceptedUsersToGroup = async ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
tx
|
tx
|
||||||
}: TAddUsersToGroup) => {
|
}: TAddUsersToGroup) => {
|
||||||
console.log("addAcceptedUsersToGroup args: ", {
|
|
||||||
userIds,
|
|
||||||
group
|
|
||||||
});
|
|
||||||
const users = await userDAL.findUserEncKeyByUserIdsBatch(
|
const users = await userDAL.findUserEncKeyByUserIdsBatch(
|
||||||
{
|
{
|
||||||
userIds
|
userIds
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas";
|
||||||
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
|
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||||
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
@@ -13,8 +16,12 @@ import {
|
|||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
@@ -23,16 +30,38 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
|||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
||||||
import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
import {
|
||||||
|
TCreateLdapCfgDTO,
|
||||||
|
TCreateLdapGroupMapDTO,
|
||||||
|
TDeleteLdapGroupMapDTO,
|
||||||
|
TGetLdapCfgDTO,
|
||||||
|
TGetLdapGroupMapsDTO,
|
||||||
|
TLdapLoginDTO,
|
||||||
|
TUpdateLdapCfgDTO
|
||||||
|
} from "./ldap-config-types";
|
||||||
|
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
|
||||||
|
|
||||||
type TLdapConfigServiceFactoryDep = {
|
type TLdapConfigServiceFactoryDep = {
|
||||||
ldapConfigDAL: TLdapConfigDALFactory;
|
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
|
||||||
|
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
|
||||||
orgDAL: Pick<
|
orgDAL: Pick<
|
||||||
TOrgDALFactory,
|
TOrgDALFactory,
|
||||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||||
>;
|
>;
|
||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
|
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
|
||||||
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
|
userGroupMembershipDAL: Pick<
|
||||||
|
TUserGroupMembershipDALFactory,
|
||||||
|
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
|
||||||
|
>;
|
||||||
|
userDAL: Pick<
|
||||||
|
TUserDALFactory,
|
||||||
|
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
|
||||||
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -42,8 +71,15 @@ export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFacto
|
|||||||
|
|
||||||
export const ldapConfigServiceFactory = ({
|
export const ldapConfigServiceFactory = ({
|
||||||
ldapConfigDAL,
|
ldapConfigDAL,
|
||||||
|
ldapGroupMapDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -60,6 +96,8 @@ export const ldapConfigServiceFactory = ({
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase,
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
caCert
|
caCert
|
||||||
}: TCreateLdapCfgDTO) => {
|
}: TCreateLdapCfgDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@@ -135,6 +173,8 @@ export const ldapConfigServiceFactory = ({
|
|||||||
bindPassIV,
|
bindPassIV,
|
||||||
bindPassTag,
|
bindPassTag,
|
||||||
searchBase,
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
encryptedCACert,
|
encryptedCACert,
|
||||||
caCertIV,
|
caCertIV,
|
||||||
caCertTag
|
caCertTag
|
||||||
@@ -154,6 +194,8 @@ export const ldapConfigServiceFactory = ({
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase,
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
caCert
|
caCert
|
||||||
}: TUpdateLdapCfgDTO) => {
|
}: TUpdateLdapCfgDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@@ -169,7 +211,9 @@ export const ldapConfigServiceFactory = ({
|
|||||||
const updateQuery: TLdapConfigsUpdate = {
|
const updateQuery: TLdapConfigsUpdate = {
|
||||||
isActive,
|
isActive,
|
||||||
url,
|
url,
|
||||||
searchBase
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter
|
||||||
};
|
};
|
||||||
|
|
||||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||||
@@ -271,6 +315,8 @@ export const ldapConfigServiceFactory = ({
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase: ldapConfig.searchBase,
|
searchBase: ldapConfig.searchBase,
|
||||||
|
groupSearchBase: ldapConfig.groupSearchBase,
|
||||||
|
groupSearchFilter: ldapConfig.groupSearchFilter,
|
||||||
caCert
|
caCert
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -305,7 +351,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
bindCredentials: ldapConfig.bindPass,
|
bindCredentials: ldapConfig.bindPass,
|
||||||
searchBase: ldapConfig.searchBase,
|
searchBase: ldapConfig.searchBase,
|
||||||
searchFilter: "(uid={{username}})",
|
searchFilter: "(uid={{username}})",
|
||||||
searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
|
// searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
|
||||||
...(ldapConfig.caCert !== ""
|
...(ldapConfig.caCert !== ""
|
||||||
? {
|
? {
|
||||||
tlsOptions: {
|
tlsOptions: {
|
||||||
@@ -320,7 +366,17 @@ export const ldapConfigServiceFactory = ({
|
|||||||
return { opts, ldapConfig };
|
return { opts, ldapConfig };
|
||||||
};
|
};
|
||||||
|
|
||||||
const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => {
|
const ldapLogin = async ({
|
||||||
|
ldapConfigId,
|
||||||
|
externalId,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
emails,
|
||||||
|
groups,
|
||||||
|
orgId,
|
||||||
|
relayState
|
||||||
|
}: TLdapLoginDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
let userAlias = await userAliasDAL.findOne({
|
let userAlias = await userAliasDAL.findOne({
|
||||||
externalId,
|
externalId,
|
||||||
@@ -394,7 +450,84 @@ export const ldapConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await userDAL.findOne({ id: userAlias.userId });
|
const user = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||||
|
if (groups) {
|
||||||
|
const ldapGroupIdsToBePartOf = (
|
||||||
|
await ldapGroupMapDAL.find({
|
||||||
|
ldapConfigId,
|
||||||
|
$in: {
|
||||||
|
ldapGroupCN: groups.map((group) => group.cn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map((groupMap) => groupMap.groupId);
|
||||||
|
|
||||||
|
const groupsToBePartOf = await groupDAL.find({
|
||||||
|
orgId,
|
||||||
|
$in: {
|
||||||
|
id: ldapGroupIdsToBePartOf
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const toBePartOfGroupIdsSet = new Set(groupsToBePartOf.map((groupToBePartOf) => groupToBePartOf.id));
|
||||||
|
|
||||||
|
const allLdapGroupMaps = await ldapGroupMapDAL.find({
|
||||||
|
ldapConfigId
|
||||||
|
});
|
||||||
|
|
||||||
|
const ldapGroupIdsCurrentlyPartOf = (
|
||||||
|
await userGroupMembershipDAL.find({
|
||||||
|
userId: newUser.id,
|
||||||
|
$in: {
|
||||||
|
groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map((userGroupMembership) => userGroupMembership.groupId);
|
||||||
|
|
||||||
|
const userGroupMembershipGroupIdsSet = new Set(ldapGroupIdsCurrentlyPartOf);
|
||||||
|
|
||||||
|
for await (const group of groupsToBePartOf) {
|
||||||
|
if (!userGroupMembershipGroupIdsSet.has(group.id)) {
|
||||||
|
// add user to group that they should be part of
|
||||||
|
await addUsersToGroupByUserIds({
|
||||||
|
group,
|
||||||
|
userIds: [newUser.id],
|
||||||
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
orgDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupsCurrentlyPartOf = await groupDAL.find({
|
||||||
|
orgId,
|
||||||
|
$in: {
|
||||||
|
id: ldapGroupIdsCurrentlyPartOf
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const group of groupsCurrentlyPartOf) {
|
||||||
|
if (!toBePartOfGroupIdsSet.has(group.id)) {
|
||||||
|
// remove user from group that they should no longer be part of
|
||||||
|
await removeUsersFromGroupByUserIds({
|
||||||
|
group,
|
||||||
|
userIds: [newUser.id],
|
||||||
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
});
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
|
||||||
@@ -424,6 +557,99 @@ export const ldapConfigServiceFactory = ({
|
|||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLdapGroupMaps = async ({
|
||||||
|
ldapConfigId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetLdapGroupMapsDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||||
|
|
||||||
|
const ldapConfig = await ldapConfigDAL.findOne({
|
||||||
|
id: ldapConfigId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||||
|
|
||||||
|
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
|
||||||
|
|
||||||
|
return groupMaps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLdapGroupMap = async ({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupCN,
|
||||||
|
groupSlug,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateLdapGroupMapDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.ldap)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create LDAP group map due to plan restriction. Upgrade plan to create LDAP group map."
|
||||||
|
});
|
||||||
|
|
||||||
|
const ldapConfig = await ldapConfigDAL.findOne({
|
||||||
|
id: ldapConfigId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||||
|
|
||||||
|
const group = await groupDAL.findOne({ slug: groupSlug, orgId });
|
||||||
|
if (!group) throw new BadRequestError({ message: "Failed to find group" });
|
||||||
|
|
||||||
|
const groupMap = await ldapGroupMapDAL.create({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupCN,
|
||||||
|
groupId: group.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLdapGroupMap = async ({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupMapId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TDeleteLdapGroupMapDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.ldap)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete LDAP group map due to plan restriction. Upgrade plan to delete LDAP group map."
|
||||||
|
});
|
||||||
|
|
||||||
|
const ldapConfig = await ldapConfigDAL.findOne({
|
||||||
|
id: ldapConfigId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||||
|
|
||||||
|
const [deletedGroupMap] = await ldapGroupMapDAL.delete({
|
||||||
|
ldapConfigId: ldapConfig.id,
|
||||||
|
id: ldapGroupMapId
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedGroupMap;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createLdapCfg,
|
createLdapCfg,
|
||||||
updateLdapCfg,
|
updateLdapCfg,
|
||||||
@@ -431,6 +657,9 @@ export const ldapConfigServiceFactory = ({
|
|||||||
getLdapCfg,
|
getLdapCfg,
|
||||||
// getLdapPassportOpts,
|
// getLdapPassportOpts,
|
||||||
ldapLogin,
|
ldapLogin,
|
||||||
bootLdap
|
bootLdap,
|
||||||
|
getLdapGroupMaps,
|
||||||
|
createLdapGroupMap,
|
||||||
|
deleteLdapGroupMap
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TLDAPConfig = {
|
||||||
|
id: string;
|
||||||
|
organization: string;
|
||||||
|
isActive: boolean;
|
||||||
|
url: string;
|
||||||
|
bindDN: string;
|
||||||
|
bindPass: string;
|
||||||
|
searchBase: string;
|
||||||
|
groupSearchBase: string;
|
||||||
|
groupSearchFilter: string;
|
||||||
|
caCert: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateLdapCfgDTO = {
|
export type TCreateLdapCfgDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -7,6 +20,8 @@ export type TCreateLdapCfgDTO = {
|
|||||||
bindDN: string;
|
bindDN: string;
|
||||||
bindPass: string;
|
bindPass: string;
|
||||||
searchBase: string;
|
searchBase: string;
|
||||||
|
groupSearchBase: string;
|
||||||
|
groupSearchFilter: string;
|
||||||
caCert: string;
|
caCert: string;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
@@ -18,6 +33,8 @@ export type TUpdateLdapCfgDTO = {
|
|||||||
bindDN: string;
|
bindDN: string;
|
||||||
bindPass: string;
|
bindPass: string;
|
||||||
searchBase: string;
|
searchBase: string;
|
||||||
|
groupSearchBase: string;
|
||||||
|
groupSearchFilter: string;
|
||||||
caCert: string;
|
caCert: string;
|
||||||
}> &
|
}> &
|
||||||
TOrgPermission;
|
TOrgPermission;
|
||||||
@@ -27,11 +44,31 @@ export type TGetLdapCfgDTO = {
|
|||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TLdapLoginDTO = {
|
export type TLdapLoginDTO = {
|
||||||
|
ldapConfigId: string;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
username: string;
|
username: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
emails: string[];
|
emails: string[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
groups?: {
|
||||||
|
dn: string;
|
||||||
|
cn: string;
|
||||||
|
}[];
|
||||||
relayState?: string;
|
relayState?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetLdapGroupMapsDTO = {
|
||||||
|
ldapConfigId: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TCreateLdapGroupMapDTO = {
|
||||||
|
ldapConfigId: string;
|
||||||
|
ldapGroupCN: string;
|
||||||
|
groupSlug: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TDeleteLdapGroupMapDTO = {
|
||||||
|
ldapConfigId: string;
|
||||||
|
ldapGroupMapId: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import ldapjs from "ldapjs";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TLDAPConfig } from "./ldap-config-types";
|
||||||
|
|
||||||
|
export const searchGroups = async (
|
||||||
|
ldapConfig: TLDAPConfig,
|
||||||
|
filter: string,
|
||||||
|
base: string
|
||||||
|
): Promise<{ dn: string; cn: string }[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ldapClient = ldapjs.createClient({
|
||||||
|
url: ldapConfig.url,
|
||||||
|
bindDN: ldapConfig.bindDN,
|
||||||
|
bindCredentials: ldapConfig.bindPass,
|
||||||
|
...(ldapConfig.caCert !== ""
|
||||||
|
? {
|
||||||
|
tlsOptions: {
|
||||||
|
ca: [ldapConfig.caCert]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
});
|
||||||
|
|
||||||
|
ldapClient.search(
|
||||||
|
base,
|
||||||
|
{
|
||||||
|
filter,
|
||||||
|
scope: "sub"
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
if (err) {
|
||||||
|
ldapClient.unbind((unbindError) => {
|
||||||
|
if (unbindError) {
|
||||||
|
logger.error("Error unbinding LDAP client:", unbindError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: { dn: string; cn: string }[] = [];
|
||||||
|
|
||||||
|
res.on("searchEntry", (entry) => {
|
||||||
|
const dn = entry.dn.toString();
|
||||||
|
const regex = /cn=([^,]+)/;
|
||||||
|
const match = dn.match(regex);
|
||||||
|
// parse the cn from the dn
|
||||||
|
const cn = (match && match[1]) as string;
|
||||||
|
|
||||||
|
groups.push({ dn, cn });
|
||||||
|
});
|
||||||
|
res.on("error", (error) => {
|
||||||
|
ldapClient.unbind((unbindError) => {
|
||||||
|
if (unbindError) {
|
||||||
|
logger.error("Error unbinding LDAP client:", unbindError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
ldapClient.unbind((unbindError) => {
|
||||||
|
if (unbindError) {
|
||||||
|
logger.error("Error unbinding LDAP client:", unbindError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve(groups);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TLdapGroupMapDALFactory = ReturnType<typeof ldapGroupMapDALFactory>;
|
||||||
|
|
||||||
|
export const ldapGroupMapDALFactory = (db: TDbClient) => {
|
||||||
|
const ldapGroupMapOrm = ormify(db, TableName.LdapGroupMap);
|
||||||
|
|
||||||
|
const findLdapGroupMapsByLdapConfigId = async (ldapConfigId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db(TableName.LdapGroupMap)
|
||||||
|
.where(`${TableName.LdapGroupMap}.ldapConfigId`, ldapConfigId)
|
||||||
|
.join(TableName.Groups, `${TableName.LdapGroupMap}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.LdapGroupMap))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
|
);
|
||||||
|
|
||||||
|
return docs.map((doc) => {
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
ldapConfigId: doc.ldapConfigId,
|
||||||
|
ldapGroupCN: doc.ldapGroupCN,
|
||||||
|
group: {
|
||||||
|
id: doc.groupId,
|
||||||
|
name: doc.groupName,
|
||||||
|
slug: doc.groupSlug
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "findGroupMaps" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...ldapGroupMapOrm, findLdapGroupMapsByLdapConfigId };
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/i
|
|||||||
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||||
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||||
|
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
|
||||||
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
||||||
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||||
@@ -200,6 +201,7 @@ export const registerRoutes = async (
|
|||||||
const samlConfigDAL = samlConfigDALFactory(db);
|
const samlConfigDAL = samlConfigDALFactory(db);
|
||||||
const scimDAL = scimDALFactory(db);
|
const scimDAL = scimDALFactory(db);
|
||||||
const ldapConfigDAL = ldapConfigDALFactory(db);
|
const ldapConfigDAL = ldapConfigDALFactory(db);
|
||||||
|
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
|
||||||
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
||||||
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
||||||
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
||||||
@@ -300,8 +302,15 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const ldapService = ldapConfigServiceFactory({
|
const ldapService = ldapConfigServiceFactory({
|
||||||
ldapConfigDAL,
|
ldapConfigDAL,
|
||||||
|
ldapGroupMapDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const authLoginServiceFactory = ({
|
|||||||
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
||||||
|
|
||||||
authMethod = decodedProviderToken.authMethod;
|
authMethod = decodedProviderToken.authMethod;
|
||||||
if (isAuthMethodSaml(authMethod) && decodedProviderToken.orgId) {
|
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) {
|
||||||
organizationId = decodedProviderToken.orgId;
|
organizationId = decodedProviderToken.orgId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
title: "LDAP"
|
|
||||||
description: "Log in to Infisical with LDAP"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Info>
|
|
||||||
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 [email protected] to purchase an enterprise license to use it.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Prepare the LDAP configuration in Infisical">
|
|
||||||
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
|
|
||||||
|
|
||||||
Next, input your LDAP server settings.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Here's some guidance for each field:
|
|
||||||
|
|
||||||
- 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 distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`.
|
|
||||||
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
|
|
||||||
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=example,dc=com`
|
|
||||||
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate.
|
|
||||||
</Step>
|
|
||||||
<Step title="Enable LDAP in Infisical">
|
|
||||||
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
|
||||||
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
@@ -4,16 +4,17 @@ description: "Learn how to log in to Infisical with LDAP."
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
LDAP is a paid feature.
|
LDAP is a paid feature. If you're using Infisical Cloud, then it is available
|
||||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
under the **Enterprise Tier**. If you're self-hosting Infisical, then you
|
||||||
then you should contact [email protected] to purchase an enterprise license to use it.
|
should contact [email protected] to purchase an enterprise license to use
|
||||||
|
it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
|
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Prepare the LDAP configuration in Infisical">
|
<Step title="Prepare the LDAP configuration in Infisical">
|
||||||
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
|
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
|
||||||
|
|
||||||
Next, input your LDAP server settings.
|
Next, input your LDAP server settings.
|
||||||
|
|
||||||
@@ -24,11 +25,41 @@ You can configure your organization in Infisical to have members authenticate wi
|
|||||||
- 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.
|
- 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 distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`.
|
- Bind DN: The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`.
|
||||||
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
|
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
|
||||||
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=example,dc=com`
|
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`
|
||||||
|
- Group Search Base / Group DN (optional): LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`.
|
||||||
|
- Group Filter (optional): Template used when constructing the group membership query such as `(objectClass=posixGroup)`. The template can access the following context variables: [`UserDN`, `UserUID`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.
|
||||||
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate.
|
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The **Group Search Base / Group DN** and **Group Filter** fields are both required if you wish to sync LDAP groups to Infisical.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Define mappings from LDAP groups to groups in Infisical">
|
||||||
|
In order to sync LDAP groups to Infisical, head to the **LDAP Group Mappings** section to define mappings from LDAP groups to groups in Infisical.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Group mappings ensure that users who log into Infisical via LDAP are added to or removed from the Infisical group(s) that corresponds to the LDAP group(s) they are a member of.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Each group mapping consists of two parts:
|
||||||
|
- LDAP Group CN: The common name of the LDAP group to map.
|
||||||
|
- Infisical Group: The Infisical group to map the LDAP group to.
|
||||||
|
|
||||||
|
For example, suppose you want to automatically add a user who is part of the LDAP group with CN `Engineers` to the Infisical group `Engineers` when the user sets up their account with Infisical.
|
||||||
|
|
||||||
|
In this case, you would specify a mapping from the LDAP group with CN `Engineers` to the Infisical group `Engineers`.
|
||||||
|
Now when the user logs into Infisical via LDAP, Infisical will check the LDAP groups that the user is a part of whilst referencing the group mappings you created earlier. Since the user is a member of the LDAP group with CN `Engineers`, they will be added to the Infisical group `Engineers`.
|
||||||
|
In the future, if the user is no longer part of the LDAP group with CN `Engineers`, they will be removed from the Infisical group `Engineers` upon their next login.
|
||||||
|
<Note>
|
||||||
|
Prior to defining any group mappings, ensure that you've created the Infisical groups that you want to map the LDAP groups to.
|
||||||
|
You can read more about creating (user) groups in Infisical [here](/documentation/platform/groups).
|
||||||
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Enable LDAP in Infisical">
|
<Step title="Enable LDAP in Infisical">
|
||||||
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
LDAP is a paid feature.
|
LDAP is a paid feature. If you're using Infisical Cloud, then it is available
|
||||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
under the **Enterprise Tier**. If you're self-hosting Infisical, then you
|
||||||
then you should contact [email protected] to purchase an enterprise license to use it.
|
should contact [email protected] to purchase an enterprise license to use
|
||||||
|
it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
@@ -17,13 +18,13 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
|
|||||||
When creating the user, input their **First Name**, **Last Name**, **Username** (required), **Company Email** (required), and **Description**.
|
When creating the user, input their **First Name**, **Last Name**, **Username** (required), **Company Email** (required), and **Description**.
|
||||||
Also, create a password for the user.
|
Also, create a password for the user.
|
||||||
|
|
||||||
Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**.
|
Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Prepare the LDAP configuration in Infisical">
|
<Step title="Prepare the LDAP configuration in Infisical">
|
||||||
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
|
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
|
||||||
|
|
||||||
Next, input your JumpCloud LDAP server settings.
|
Next, input your JumpCloud LDAP server settings.
|
||||||
|
|
||||||
@@ -35,20 +36,48 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
|
|||||||
- Bind DN: The distinguished name of object to bind when performing the user search (`uid=<ldap-user-username>,ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
|
- Bind DN: The distinguished name of object to bind when performing the user search (`uid=<ldap-user-username>,ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
|
||||||
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
|
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
|
||||||
- Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
|
- Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
|
||||||
|
- Group Search Base / Group DN (optional): LDAP search base to use for group membership search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
|
||||||
|
- Group Filter (optional): Template used when constructing the group membership query (`(objectClass=groupOfNames)`).
|
||||||
- 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)).
|
- 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)).
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
When filling out the **Bind DN** and **Bind Pass** fields, refer to the username and password of the user created in Step 1.
|
When filling out the **Bind DN** and **Bind Pass** fields, refer to the username and password of the user created in Step 1.
|
||||||
|
|
||||||
Also, for the **Bind DN** and **Search Base / User DN** fields, you'll want to use the organization ID that appears
|
Also, for the **Bind DN** and **Search Base / User DN** fields, you'll want to use the organization ID that appears
|
||||||
in your LDAP instance **ORG DN**.
|
in your LDAP instance **ORG DN**.
|
||||||
</Tip>
|
</Tip>
|
||||||
</Step>
|
</Step>
|
||||||
|
<Step title="Define mappings from LDAP groups to groups in Infisical">
|
||||||
|
In order to sync LDAP groups to Infisical, head to the **LDAP Group Mappings** section to define mappings from LDAP groups to groups in Infisical.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Group mappings ensure that users who log into Infisical via LDAP are added to or removed from the Infisical group(s) that corresponds to the LDAP group(s) they are a member of.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Each group mapping consists of two parts:
|
||||||
|
- LDAP Group CN: The common name of the LDAP group to map.
|
||||||
|
- Infisical Group: The Infisical group to map the LDAP group to.
|
||||||
|
|
||||||
|
For example, suppose you want to automatically add a user who is part of the LDAP group with CN `Engineers` to the Infisical group `Engineers` when the user sets up their account with Infisical.
|
||||||
|
|
||||||
|
In this case, you would specify a mapping from the LDAP group with CN `Engineers` to the Infisical group `Engineers`.
|
||||||
|
Now when the user logs into Infisical via LDAP, Infisical will check the LDAP groups that the user is a part of whilst referencing the group mappings you created earlier. Since the user is a member of the LDAP group with CN `Engineers`, they will be added to the Infisical group `Engineers`.
|
||||||
|
In the future, if the user is no longer part of the LDAP group with CN `Engineers`, they will be removed from the Infisical group `Engineers` upon their next login.
|
||||||
|
<Note>
|
||||||
|
Prior to defining any group mappings, ensure that you've created the Infisical groups that you want to map the LDAP groups to.
|
||||||
|
You can read more about creating (user) groups in Infisical [here](/documentation/platform/groups).
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Step>
|
||||||
<Step title="Enable LDAP in Infisical">
|
<Step title="Enable LDAP in Infisical">
|
||||||
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
- [JumpCloud Cloud LDAP Guide](https://jumpcloud.com/support/use-cloud-ldap)
|
|
||||||
|
- [JumpCloud Cloud LDAP Guide](https://jumpcloud.com/support/use-cloud-ldap)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 439 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 721 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 456 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 537 KiB After Width: | Height: | Size: 599 KiB |
@@ -1 +1,6 @@
|
|||||||
export { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "./queries";
|
export {
|
||||||
|
useCreateLDAPConfig,
|
||||||
|
useCreateLDAPGroupMapping,
|
||||||
|
useDeleteLDAPGroupMapping,
|
||||||
|
useUpdateLDAPConfig} from "./mutations";
|
||||||
|
export { useGetLDAPConfig, useGetLDAPGroupMaps } from "./queries";
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { ldapConfigKeys } from "./queries";
|
||||||
|
|
||||||
|
export const useCreateLDAPConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
organizationId,
|
||||||
|
isActive,
|
||||||
|
url,
|
||||||
|
bindDN,
|
||||||
|
bindPass,
|
||||||
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
|
caCert
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
url: string;
|
||||||
|
bindDN: string;
|
||||||
|
bindPass: string;
|
||||||
|
searchBase: string;
|
||||||
|
groupSearchBase: string;
|
||||||
|
groupSearchFilter: string;
|
||||||
|
caCert?: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/ldap/config", {
|
||||||
|
organizationId,
|
||||||
|
isActive,
|
||||||
|
url,
|
||||||
|
bindDN,
|
||||||
|
bindPass,
|
||||||
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
|
caCert
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess(_, dto) {
|
||||||
|
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateLDAPConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
organizationId,
|
||||||
|
isActive,
|
||||||
|
url,
|
||||||
|
bindDN,
|
||||||
|
bindPass,
|
||||||
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
|
caCert
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
url?: string;
|
||||||
|
bindDN?: string;
|
||||||
|
bindPass?: string;
|
||||||
|
searchBase?: string;
|
||||||
|
groupSearchBase?: string;
|
||||||
|
groupSearchFilter?: string;
|
||||||
|
caCert?: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.patch("/api/v1/ldap/config", {
|
||||||
|
organizationId,
|
||||||
|
isActive,
|
||||||
|
url,
|
||||||
|
bindDN,
|
||||||
|
bindPass,
|
||||||
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
|
caCert
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess(_, dto) {
|
||||||
|
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateLDAPGroupMapping = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupCN,
|
||||||
|
groupSlug
|
||||||
|
}: {
|
||||||
|
ldapConfigId: string;
|
||||||
|
ldapGroupCN: string;
|
||||||
|
groupSlug: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v1/ldap/config/${ldapConfigId}/group-maps`, {
|
||||||
|
ldapGroupCN,
|
||||||
|
groupSlug
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess(_, { ldapConfigId }) {
|
||||||
|
queryClient.invalidateQueries(ldapConfigKeys.getLDAPGroupMaps(ldapConfigId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteLDAPGroupMapping = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupMapId
|
||||||
|
}: {
|
||||||
|
ldapConfigId: string;
|
||||||
|
ldapGroupMapId: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
`/api/v1/ldap/config/${ldapConfigId}/group-maps/${ldapGroupMapId}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess(_, { ldapConfigId }) {
|
||||||
|
queryClient.invalidateQueries(ldapConfigKeys.getLDAPGroupMaps(ldapConfigId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
const ldapConfigKeys = {
|
import { LDAPGroupMap } from "./types";
|
||||||
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const
|
|
||||||
|
export const ldapConfigKeys = {
|
||||||
|
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const,
|
||||||
|
getLDAPGroupMaps: (ldapConfigId: string) => [{ ldapConfigId }, "ldap-group-maps"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetLDAPConfig = (organizationId: string) => {
|
export const useGetLDAPConfig = (organizationId: string) => {
|
||||||
@@ -18,78 +21,18 @@ export const useGetLDAPConfig = (organizationId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateLDAPConfig = () => {
|
export const useGetLDAPGroupMaps = (ldapConfigId: string) => {
|
||||||
const queryClient = useQueryClient();
|
return useQuery({
|
||||||
return useMutation({
|
queryKey: ldapConfigKeys.getLDAPGroupMaps(ldapConfigId),
|
||||||
mutationFn: async ({
|
queryFn: async () => {
|
||||||
organizationId,
|
if (!ldapConfigId) return [];
|
||||||
isActive,
|
|
||||||
url,
|
const { data } = await apiRequest.get<LDAPGroupMap[]>(
|
||||||
bindDN,
|
`/api/v1/ldap/config/${ldapConfigId}/group-maps`
|
||||||
bindPass,
|
);
|
||||||
searchBase,
|
|
||||||
caCert
|
|
||||||
}: {
|
|
||||||
organizationId: string;
|
|
||||||
isActive: boolean;
|
|
||||||
url: string;
|
|
||||||
bindDN: string;
|
|
||||||
bindPass: string;
|
|
||||||
searchBase: string;
|
|
||||||
caCert?: string;
|
|
||||||
}) => {
|
|
||||||
const { data } = await apiRequest.post("/api/v1/ldap/config", {
|
|
||||||
organizationId,
|
|
||||||
isActive,
|
|
||||||
url,
|
|
||||||
bindDN,
|
|
||||||
bindPass,
|
|
||||||
searchBase,
|
|
||||||
caCert
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
enabled: true
|
||||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateLDAPConfig = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({
|
|
||||||
organizationId,
|
|
||||||
isActive,
|
|
||||||
url,
|
|
||||||
bindDN,
|
|
||||||
bindPass,
|
|
||||||
searchBase,
|
|
||||||
caCert
|
|
||||||
}: {
|
|
||||||
organizationId: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
url?: string;
|
|
||||||
bindDN?: string;
|
|
||||||
bindPass?: string;
|
|
||||||
searchBase?: string;
|
|
||||||
caCert?: string;
|
|
||||||
}) => {
|
|
||||||
const { data } = await apiRequest.patch("/api/v1/ldap/config", {
|
|
||||||
organizationId,
|
|
||||||
isActive,
|
|
||||||
url,
|
|
||||||
bindDN,
|
|
||||||
bindPass,
|
|
||||||
searchBase,
|
|
||||||
caCert
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
onSuccess(_, dto) {
|
|
||||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type LDAPGroupMap = {
|
||||||
|
id: string;
|
||||||
|
ldapConfigId: string;
|
||||||
|
ldapGroupCN: string;
|
||||||
|
group: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faUsers, 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,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateLDAPGroupMapping,
|
||||||
|
useDeleteLDAPGroupMapping,
|
||||||
|
useGetLDAPConfig,
|
||||||
|
useGetLDAPGroupMaps,
|
||||||
|
useGetOrganizationGroups
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
ldapGroupCN: z.string().min(1, "LDAP Group CN is required"),
|
||||||
|
groupSlug: z.string().min(1, "Group Slug is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["ldapGroupMap", "deleteLdapGroupMap"]>;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteLdapGroupMap"]>,
|
||||||
|
data?: {
|
||||||
|
ldapGroupMapId: string;
|
||||||
|
ldapGroupCN: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["ldapGroupMap", "deleteLdapGroupMap"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: ldapConfig } = useGetLDAPConfig(currentOrg?.id ?? "");
|
||||||
|
const { data: groups } = useGetOrganizationGroups(currentOrg?.id ?? "");
|
||||||
|
const { data: groupMaps, isLoading } = useGetLDAPGroupMaps(ldapConfig?.id ?? "");
|
||||||
|
const { mutateAsync: createLDAPGroupMapping, isLoading: createIsLoading } =
|
||||||
|
useCreateLDAPGroupMapping();
|
||||||
|
const { mutateAsync: deleteLDAPGroupMapping } = useDeleteLDAPGroupMapping();
|
||||||
|
|
||||||
|
const { control, handleSubmit, reset } = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
ldapGroupCN: "",
|
||||||
|
groupSlug: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ groupSlug, ldapGroupCN }: TFormData) => {
|
||||||
|
try {
|
||||||
|
if (!ldapConfig) return;
|
||||||
|
|
||||||
|
await createLDAPGroupMapping({
|
||||||
|
ldapConfigId: ldapConfig.id,
|
||||||
|
groupSlug,
|
||||||
|
ldapGroupCN
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully added LDAP group mapping for ${ldapGroupCN}`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to add LDAP group mapping for ${ldapGroupCN}`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteGroupMapSubmit = async ({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupMapId,
|
||||||
|
ldapGroupCN
|
||||||
|
}: {
|
||||||
|
ldapConfigId: string;
|
||||||
|
ldapGroupMapId: string;
|
||||||
|
ldapGroupCN: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await deleteLDAPGroupMapping({
|
||||||
|
ldapConfigId,
|
||||||
|
ldapGroupMapId
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("deleteLdapGroupMap", false);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully deleted LDAP group mapping ${ldapGroupCN}`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to delete LDAP group mapping ${ldapGroupCN}`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.ldapGroupMap?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("ldapGroupMap", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title="Manage LDAP Group Mappings">
|
||||||
|
<h2 className="mb-4">New Group Mapping</h2>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||||
|
<div className="flex">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="ldapGroupCN"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="LDAP Group CN"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Engineering" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="groupSlug"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Infisical Group"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="ml-4 w-full"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(groups || []).map(({ name, id, slug }) => (
|
||||||
|
<SelectItem value={slug} key={`internal-group-${id}`}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button className="ml-4" size="sm" type="submit" isLoading={createIsLoading}>
|
||||||
|
Add mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h2 className="mb-4">Group Mappings</h2>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>LDAP Group CN</Th>
|
||||||
|
<Th>Infisical Group</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={3} innerKey="ldap-group-maps" />}
|
||||||
|
{!isLoading &&
|
||||||
|
groupMaps?.map(({ id, ldapGroupCN, group }) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10 items-center" key={`ldap-group-map-${id}`}>
|
||||||
|
<Td>{ldapGroupCN}</Td>
|
||||||
|
<Td>{group.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("deleteLdapGroupMap", {
|
||||||
|
ldapGroupMapId: id,
|
||||||
|
ldapGroupCN
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{groupMaps?.length === 0 && (
|
||||||
|
<EmptyState title="No LDAP group mappings found" icon={faUsers} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteLdapGroupMap.isOpen}
|
||||||
|
title={`Are you sure want to delete the group mapping for ${
|
||||||
|
(popUp?.deleteLdapGroupMap?.data as { ldapGroupCN: string })?.ldapGroupCN || ""
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteLdapGroupMap", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => {
|
||||||
|
const deleteLdapGroupMapData = popUp?.deleteLdapGroupMap?.data as {
|
||||||
|
ldapGroupMapId: string;
|
||||||
|
ldapGroupCN: string;
|
||||||
|
};
|
||||||
|
return onDeleteGroupMapSubmit({
|
||||||
|
ldapConfigId: ldapConfig?.id ?? "",
|
||||||
|
ldapGroupMapId: deleteLdapGroupMapData.ldapGroupMapId,
|
||||||
|
ldapGroupCN: deleteLdapGroupMapData.ldapGroupCN
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,6 +14,8 @@ const LDAPFormSchema = z.object({
|
|||||||
bindDN: z.string().default(""),
|
bindDN: z.string().default(""),
|
||||||
bindPass: z.string().default(""),
|
bindPass: z.string().default(""),
|
||||||
searchBase: z.string().default(""),
|
searchBase: z.string().default(""),
|
||||||
|
groupSearchBase: z.string().default(""),
|
||||||
|
groupSearchFilter: z.string().default(""),
|
||||||
caCert: z.string().optional()
|
caCert: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ type Props = {
|
|||||||
|
|
||||||
export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
|
export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig();
|
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig();
|
||||||
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateLDAPConfig();
|
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateLDAPConfig();
|
||||||
const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
|
const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
|
||||||
@@ -43,12 +45,22 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
|||||||
bindDN: data?.bindDN ?? "",
|
bindDN: data?.bindDN ?? "",
|
||||||
bindPass: data?.bindPass ?? "",
|
bindPass: data?.bindPass ?? "",
|
||||||
searchBase: data?.searchBase ?? "",
|
searchBase: data?.searchBase ?? "",
|
||||||
|
groupSearchBase: data?.groupSearchBase ?? "",
|
||||||
|
groupSearchFilter: data?.groupSearchFilter ?? "",
|
||||||
caCert: data?.caCert ?? ""
|
caCert: data?.caCert ?? ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const onSSOModalSubmit = async ({ url, bindDN, bindPass, searchBase, caCert }: TLDAPFormData) => {
|
const onSSOModalSubmit = async ({
|
||||||
|
url,
|
||||||
|
bindDN,
|
||||||
|
bindPass,
|
||||||
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
|
caCert
|
||||||
|
}: TLDAPFormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
|
|
||||||
@@ -60,6 +72,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase,
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
caCert
|
caCert
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -70,6 +84,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase,
|
searchBase,
|
||||||
|
groupSearchBase,
|
||||||
|
groupSearchFilter,
|
||||||
caCert
|
caCert
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -139,6 +155,32 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="groupSearchBase"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Group Search Base / Group DN (Optional)"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="ou=groups,dc=acme,dc=com" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="groupSearchFilter"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Group Filter (Optional)"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="(objectClass=posixGroup)" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="caCert"
|
name="caCert"
|
||||||
|
|||||||
+41
-2
@@ -10,16 +10,20 @@ import {
|
|||||||
import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api";
|
import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { LDAPGroupMapModal } from "./LDAPGroupMapModal";
|
||||||
import { LDAPModal } from "./LDAPModal";
|
import { LDAPModal } from "./LDAPModal";
|
||||||
|
|
||||||
export const OrgLDAPSection = (): JSX.Element => {
|
export const OrgLDAPSection = (): JSX.Element => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
|
const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
|
||||||
|
|
||||||
const { mutateAsync } = useUpdateLDAPConfig();
|
const { mutateAsync } = useUpdateLDAPConfig();
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addLDAP",
|
"addLDAP",
|
||||||
|
"ldapGroupMap",
|
||||||
|
"deleteLdapGroupMap",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
@@ -63,7 +67,9 @@ export const OrgLDAPSection = (): JSX.Element => {
|
|||||||
url: "",
|
url: "",
|
||||||
bindDN: "",
|
bindDN: "",
|
||||||
bindPass: "",
|
bindPass: "",
|
||||||
searchBase: ""
|
searchBase: "",
|
||||||
|
groupSearchBase: "",
|
||||||
|
groupSearchFilter: ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +82,15 @@ export const OrgLDAPSection = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openLDAPGroupMapModal = () => {
|
||||||
|
if (!subscription?.ldap) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("ldapGroupMap");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr className="border-mineshaft-600" />
|
<hr className="border-mineshaft-600" />
|
||||||
@@ -92,6 +107,25 @@ export const OrgLDAPSection = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
|
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={openLDAPGroupMapModal}
|
||||||
|
colorSchema="secondary"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
Manage how LDAP groups are mapped to internal groups in Infisical
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{data && (
|
{data && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
@@ -119,6 +153,11 @@ export const OrgLDAPSection = (): JSX.Element => {
|
|||||||
handlePopUpClose={handlePopUpClose}
|
handlePopUpClose={handlePopUpClose}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
|
<LDAPGroupMapModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
Reference in New Issue
Block a user