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 a1617d57e..11d045eb2 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1352,6 +1352,7 @@ interface AddIdentityLdapAuthEvent { accessTokenTrustedIps?: Array; allowedFields?: TAllowedFields[]; url: string; + templateId?: string | null; }; } @@ -1365,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent { accessTokenTrustedIps?: Array; allowedFields?: TAllowedFields[]; url?: string; + templateId?: string | null; }; } diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index adc240358..7835cd19b 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -33,7 +33,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions { EditTemplates = "edit-templates", CreateTemplates = "create-templates", DeleteTemplates = "delete-templates", - UnlinkTemplates = "unlink-templates" + UnlinkTemplates = "unlink-templates", + UseTemplates = "use-templates" } export enum OrgPermissionAdminConsoleAction { @@ -382,6 +383,7 @@ const buildAdminPermission = () => { OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate ); + can(OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare); @@ -423,6 +425,7 @@ const buildMemberPermission = () => { OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate ); + can(OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); return rules; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a0a7920fa..b4bdcb4fa 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -148,42 +148,6 @@ export const IDENTITIES = { } } as const; -export const IDENTITY_TEMPLATES = { - CREATE: { - name: "The name of the identity template to create.", - organizationId: "The organization ID to which the identity template belongs.", - authMethod: "The auth method of the template.", - credentials: "Set of credentials to reuse by this template" - }, - UPDATE: { - identityTemplateId: "The ID of the identity to update.", - name: "The new name of the identity template.", - authMethod: "The auth method of the template." - }, - DELETE: { - identityTemplateId: "The ID of the identity template to delete." - }, - GET_BY_ID: { - identityTemplateId: "The ID of the identity template to get details.", - organizationId: "The organization ID to which the identity template belongs.", - authMethod: "The auth method of the template.", - credentials: "Set of credentials used by this template" - }, - LIST: { - orgId: "The ID of the organization to list identity templates." - }, - SEARCH: { - search: { - desc: "The filters to apply to the search.", - name: "The name of the identity template to filter by." - }, - offset: "The offset to start from. If you enter 10, it will start from the 10th identity.", - limit: "The number of identity templates to return.", - orderBy: "The column to order identity templates by.", - orderDirection: "The direction to order identity templates in." - } -}; - export const UNIVERSAL_AUTH = { LOGIN: { clientId: "Your Machine Identity Client ID.", @@ -251,7 +215,7 @@ export const LDAP_AUTH = { password: "The password of the LDAP user to login." }, ATTACH: { - templateId: "The ID of the template to attach the configuration onto.", + templateId: "The ID of the identity auth template to attach the configuration onto.", identityId: "The ID of the identity to attach the configuration onto.", url: "The URL of the LDAP server.", allowedFields: @@ -279,7 +243,7 @@ export const LDAP_AUTH = { accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", - templateId: "The ID of the template to update the configuration for." + templateId: "The ID of the identity auth template to update the configuration to." }, RETRIEVE: { identityId: "The ID of the identity to retrieve the configuration for." diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c93a12ab6..bf5d0d1b3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1469,7 +1469,8 @@ export const registerRoutes = async ( identityLdapAuthDAL, permissionService, kmsService, - licenseService + licenseService, + auditLogService }); const identityAccessTokenService = identityAccessTokenServiceFactory({ diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index c9bae9fc4..13710a44d 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -200,55 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId) }), - body: z - .object({ - templateId: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.templateId), - url: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.url), - bindDN: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.bindDN), - bindPass: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.bindPass), - searchBase: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.searchBase), - searchFilter: z - .string() - .trim() - .min(1) - .default("(uid={{username}})") - .refine(isValidLdapFilter, "Invalid LDAP search filter") - .describe(LDAP_AUTH.ATTACH.searchFilter), - allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), - ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(0) - .max(315360000) - .default(2592000) - .describe(LDAP_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .min(1) - .max(315360000) - .default(2592000) - .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) - }) - .refine( - (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, - "Access Token TTL cannot be greater than Access Token Max TTL." - ) - .refine((val) => { - const hasTemplateId = !!val.templateId; - const hasManualConfig = !!(val.url && val.bindDN && val.bindPass && val.searchBase); - return hasTemplateId || hasManualConfig; - }, "Either templateId must be provided, or all of url, bindDN, bindPass, and searchBase must be provided."), + body: z.union([ + // Template-based configuration + z + .object({ + templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId), + searchFilter: z + .string() + .trim() + .min(1) + .default("(uid={{username}})") + .refine(isValidLdapFilter, "Invalid LDAP search filter") + .describe(LDAP_AUTH.ATTACH.searchFilter), + allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), + ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + + // Manual configuration + z + .object({ + url: z.string().trim().describe(LDAP_AUTH.ATTACH.url), + bindDN: z.string().trim().describe(LDAP_AUTH.ATTACH.bindDN), + bindPass: z.string().trim().describe(LDAP_AUTH.ATTACH.bindPass), + searchBase: z.string().trim().describe(LDAP_AUTH.ATTACH.searchBase), + searchFilter: z + .string() + .trim() + .min(1) + .default("(uid={{username}})") + .refine(isValidLdapFilter, "Invalid LDAP search filter") + .describe(LDAP_AUTH.ATTACH.searchFilter), + allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), + ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ) + ]), response: { 200: z.object({ identityLdapAuth: IdentityLdapAuthsSchema.omit({ @@ -281,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, - allowedFields: req.body.allowedFields + allowedFields: req.body.allowedFields, + templateId: identityLdapAuth.templateId } } }); @@ -383,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], - allowedFields: req.body.allowedFields + allowedFields: req.body.allowedFields, + templateId: identityLdapAuth.templateId } } }); diff --git a/backend/src/server/routes/v1/identity-template-router.ts b/backend/src/server/routes/v1/identity-template-router.ts index ec1823967..3f14d0208 100644 --- a/backend/src/server/routes/v1/identity-template-router.ts +++ b/backend/src/server/routes/v1/identity-template-router.ts @@ -12,10 +12,11 @@ import { } from "@app/services/identity-auth-template/identity-auth-template-enums"; const ldapTemplateFieldsSchema = z.object({ - url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP_URL_REQUIRED), - bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.BIND_DN_REQUIRED), - bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.BIND_PASSWORD_REQUIRED), - searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.SEARCH_BASE_REQUIRED) + url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED), + bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED), + bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED), + searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED), + ldapCaCertificate: z.string().trim().optional() }); export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => { @@ -44,8 +45,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) templateFields: ldapTemplateFieldsSchema }), response: { - 200: z.object({ - message: z.string() + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: z.record(z.string(), z.unknown()) }) } }, @@ -72,7 +73,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) } }); - return { message: TEMPLATE_SUCCESS_MESSAGES.CREATED }; + return template; } }); @@ -104,8 +105,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) templateFields: ldapTemplateFieldsSchema.partial().optional() }), response: { - 200: z.object({ - message: z.string() + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: z.record(z.string(), z.unknown()) }) } }, @@ -132,7 +133,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) } }); - return { message: TEMPLATE_SUCCESS_MESSAGES.UPDATED }; + return template; } }); @@ -204,10 +205,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED) }), response: { - 200: z.object({ - template: IdentityAuthTemplatesSchema.extend({ - templateFields: ldapTemplateFieldsSchema - }) + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: ldapTemplateFieldsSchema }) } }, @@ -220,7 +219,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) actorOrgId: req.permission.orgId }); - return { template }; + return template; } }); @@ -347,7 +346,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) server.route({ method: "POST", - url: "/:templateId/usage", + url: "/:templateId/delete-usage", config: { rateLimit: writeLimit }, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 3d0e721c4..c28d2788e 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -135,13 +135,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerIntegrationRouter, { prefix: "/integration" }); await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" }); await server.register(registerWebhookRouter, { prefix: "/webhooks" }); - await server.register( - async (identitiesRouter) => { - await identitiesRouter.register(registerIdentityRouter); - await identitiesRouter.register(registerIdentityTemplateRouter, { prefix: "/templates" }); - }, - { prefix: "/identities" } - ); + await server.register(registerIdentityRouter, { prefix: "/identities" }); + await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" }); await server.register( async (secretSharingRouter) => { diff --git a/backend/src/services/identity-auth-template/identity-auth-template-enums.ts b/backend/src/services/identity-auth-template/identity-auth-template-enums.ts index 36cdefb5d..c5b47b158 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-enums.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-enums.ts @@ -7,10 +7,12 @@ export const TEMPLATE_VALIDATION_MESSAGES = { TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long", AUTH_METHOD_REQUIRED: "Auth method is required", TEMPLATE_ID_REQUIRED: "Template ID is required", - LDAP_URL_REQUIRED: "LDAP URL is required", - BIND_DN_REQUIRED: "Bind DN is required", - BIND_PASSWORD_REQUIRED: "Bind password is required", - SEARCH_BASE_REQUIRED: "Search base is required" + LDAP: { + URL_REQUIRED: "LDAP URL is required", + BIND_DN_REQUIRED: "Bind DN is required", + BIND_PASSWORD_REQUIRED: "Bind password is required", + SEARCH_BASE_REQUIRED: "Search base is required" + } } as const; export const TEMPLATE_SUCCESS_MESSAGES = { diff --git a/backend/src/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/services/identity-auth-template/identity-auth-template-service.ts index e55c59a4a..2920d309c 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-service.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionMachineIdentityAuthTemplateActions, @@ -9,6 +10,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TOrgPermission } from "@app/lib/types"; +import { ActorType } from "../auth/auth-type"; import { TIdentityLdapAuthDALFactory } from "../identity-ldap-auth/identity-ldap-auth-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; @@ -30,6 +32,7 @@ type TIdentityAuthTemplateServiceFactoryDep = { permissionService: Pick; kmsService: Pick; licenseService: Pick; + auditLogService: Pick; }; export type TIdentityAuthTemplateServiceFactory = ReturnType; @@ -39,7 +42,8 @@ export const identityAuthTemplateServiceFactory = ({ identityLdapAuthDAL, permissionService, kmsService, - licenseService + licenseService, + auditLogService }: TIdentityAuthTemplateServiceFactoryDep) => { // Plan check const $checkPlan = async (orgId: string) => { @@ -87,7 +91,7 @@ export const identityAuthTemplateServiceFactory = ({ orgId: actorOrgId }); - return template; + return { ...template, templateFields }; }; const updateTemplate = async ({ @@ -104,7 +108,7 @@ export const identityAuthTemplateServiceFactory = ({ templateFields?: Record; } & Omit) => { await $checkPlan(actorOrgId); - const template = await identityAuthTemplateDAL.findById(templateId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); if (!template) { throw new NotFoundError({ message: "Template not found" }); } @@ -126,6 +130,8 @@ export const identityAuthTemplateServiceFactory = ({ orgId: template.orgId }); + let finalTemplateFields: Record = {}; + const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => { const authTemplate = await identityAuthTemplateDAL.updateById( templateId, @@ -149,12 +155,13 @@ export const identityAuthTemplateServiceFactory = ({ ) as TLdapTemplateFields; const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields }; - + finalTemplateFields = mergedTemplateFields; const ldapUpdateData: { url?: string; searchBase?: string; encryptedBindDN?: Buffer; encryptedBindPass?: Buffer; + encryptedLdapCaCertificate?: Buffer; } = {}; if ("url" in templateFields) { @@ -173,16 +180,38 @@ export const identityAuthTemplateServiceFactory = ({ plainText: Buffer.from(mergedTemplateFields.bindPass) }).cipherTextBlob; } + if ("ldapCaCertificate" in templateFields) { + ldapUpdateData.encryptedLdapCaCertificate = encryptor({ + plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "") + }).cipherTextBlob; + } if (Object.keys(ldapUpdateData).length > 0) { - await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx); + const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx); + await Promise.all( + updatedLdapAuths.map(async (updatedLdapAuth) => { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId: actorOrgId, + event: { + type: EventType.UPDATE_IDENTITY_LDAP_AUTH, + metadata: { + identityId: updatedLdapAuth.identityId, + templateId: template.id + } + } + }); + }) + ); } } - return authTemplate; }); - return updatedTemplate; + return { ...updatedTemplate, templateFields: finalTemplateFields }; }; const deleteTemplate = async ({ @@ -193,7 +222,7 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TDeleteIdentityAuthTemplateDTO) => { await $checkPlan(actorOrgId); - const template = await identityAuthTemplateDAL.findById(templateId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); if (!template) { throw new NotFoundError({ message: "Template not found" }); } @@ -212,7 +241,25 @@ export const identityAuthTemplateServiceFactory = ({ const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => { // Remove template reference from identityLdapAuth records - await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx); + const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx); + await Promise.all( + updatedLdapAuths.map(async (updatedLdapAuth) => { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId: actorOrgId, + event: { + type: EventType.UPDATE_IDENTITY_LDAP_AUTH, + metadata: { + identityId: updatedLdapAuth.identityId, + templateId: template.id + } + } + }); + }) + ); // Delete the template const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx); @@ -230,7 +277,7 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TGetIdentityAuthTemplateDTO) => { await $checkPlan(actorOrgId); - const template = await identityAuthTemplateDAL.findById(templateId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); if (!template) { throw new NotFoundError({ message: "Template not found" }); } @@ -313,7 +360,7 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, + OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate ); @@ -350,7 +397,7 @@ export const identityAuthTemplateServiceFactory = ({ OrgPermissionSubjects.MachineIdentityAuthTemplate ); - const template = await identityAuthTemplateDAL.findById(templateId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); if (!template) { throw new NotFoundError({ message: "Template not found" }); } @@ -376,11 +423,11 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, + OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate ); - const template = await identityAuthTemplateDAL.findById(templateId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); if (!template) { throw new NotFoundError({ message: "Template not found" }); } diff --git a/backend/src/services/identity-auth-template/identity-auth-template-types.ts b/backend/src/services/identity-auth-template/identity-auth-template-types.ts index 5c4b4a73f..8039e41c2 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-types.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-types.ts @@ -8,6 +8,7 @@ export type TLdapTemplateFields = { bindDN: string; bindPass: string; searchBase: string; + ldapCaCertificate?: string; }; // Union type for all template field types 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 index e108554a7..25452f3ba 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -4,7 +4,11 @@ import { ForbiddenError } from "@casl/ability"; 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 { + OrgPermissionIdentityActions, + OrgPermissionMachineIdentityAuthTemplateActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation @@ -217,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + if (templateId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + } + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); if (!plan.ldap) { @@ -254,7 +266,7 @@ export const identityLdapAuthServiceFactory = ({ ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) : undefined; - let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string }; + let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string }; if (template) { ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString()); } else { @@ -267,7 +279,8 @@ export const identityLdapAuthServiceFactory = ({ bindDN, bindPass, searchBase, - url + url, + ldapCaCertificate }; } @@ -280,9 +293,9 @@ export const identityLdapAuthServiceFactory = ({ }); let encryptedLdapCaCertificate: Buffer | undefined; - if (ldapCaCertificate) { + if (ldapConfig.ldapCaCertificate) { const { cipherTextBlob: encryptedCertificate } = encryptor({ - plainText: Buffer.from(ldapCaCertificate) + plainText: Buffer.from(ldapConfig.ldapCaCertificate) }); encryptedLdapCaCertificate = encryptedCertificate; @@ -291,7 +304,7 @@ export const identityLdapAuthServiceFactory = ({ const isConnected = await testLDAPConfig({ bindDN: ldapConfig.bindDN, bindPass: ldapConfig.bindPass, - caCert: ldapCaCertificate || "", + caCert: ldapConfig.ldapCaCertificate || "", url: ldapConfig.url }); @@ -371,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (templateId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + } + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); if (!plan.ldap) { @@ -411,6 +431,7 @@ export const identityLdapAuthServiceFactory = ({ bindPass?: string; searchBase?: string; url?: string; + ldapCaCertificate?: string; }; if (template) { @@ -420,7 +441,8 @@ export const identityLdapAuthServiceFactory = ({ bindDN, bindPass, searchBase, - url + url, + ldapCaCertificate }; } @@ -434,9 +456,9 @@ export const identityLdapAuthServiceFactory = ({ } let encryptedLdapCaCertificate: Buffer | undefined; - if (ldapCaCertificate) { + if (config.ldapCaCertificate) { const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({ - plainText: Buffer.from(ldapCaCertificate) + plainText: Buffer.from(config.ldapCaCertificate) }); encryptedLdapCaCertificate = ldapCaCertificateCiphertext; @@ -456,7 +478,7 @@ export const identityLdapAuthServiceFactory = ({ const isConnected = await testLDAPConfig({ bindDN: config.bindDN || ldapConfig.bindDN, bindPass: config.bindPass || ldapConfig.bindPass, - caCert: ldapCaCertificate || ldapConfig.caCert, + caCert: config.ldapCaCertificate || ldapConfig.caCert, url: config.url || ldapConfig.url }); diff --git a/docs/docs.json b/docs/docs.json index 3d6d1daaf..b6a4e24a0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -291,7 +291,6 @@ { "group": "Machine Identities", "pages": [ - "documentation/platform/identities/auth-templates", "documentation/platform/identities/alicloud-auth", "documentation/platform/identities/aws-auth", "documentation/platform/identities/azure-auth", @@ -323,6 +322,7 @@ } ] }, + "documentation/platform/identities/auth-templates", "documentation/platform/token", "documentation/platform/mfa", "documentation/platform/github-org-sync" diff --git a/docs/documentation/platform/identities/auth-templates.mdx b/docs/documentation/platform/identities/auth-templates.mdx index 0052e524a..ca857bc26 100644 --- a/docs/documentation/platform/identities/auth-templates.mdx +++ b/docs/documentation/platform/identities/auth-templates.mdx @@ -67,16 +67,16 @@ Auth templates are managed in **Organization Settings > Access Control > Identit Once created, templates can be applied when configuring authentication methods for machine identities. When adding an auth method to an identity, you'll have the option to select from available templates or configure manually. +![Attach Template](/images/platform/identities/auth-templates/machine-identity-page.png) +![Attach Template Form](/images/platform/identities/auth-templates/attach-template-form.png) + ### Managing Template Usage -You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu. This allows you to: +You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu. ![Template Usages](/images/platform/identities/auth-templates/template-usages.png) ![Template Usages Modal](/images/platform/identities/auth-templates/template-usages-modal.png) -- See all identities currently using the template -- Unlink identities from the template - ## FAQ @@ -85,11 +85,11 @@ You can view which identities are using a specific template by clicking **View U - If you delete a template that's currently being used by identities, those identities will continue to function with their existing configuration. + If you delete a template that's currently being used by identities, those identities will continue to function with their existing configuration. However, the link to the template will be broken, and you won't be able to use the template for new identities. - Yes, click **View Usages** in the template's dropdown menu to see all identities currently using that template. You can also unlink identities from templates from this view. + Yes, click **View Usages** in the template's dropdown menu to see all identities currently using that template. diff --git a/docs/documentation/platform/identities/ldap-auth/general.mdx b/docs/documentation/platform/identities/ldap-auth/general.mdx index 7fb2798c7..01395b68c 100644 --- a/docs/documentation/platform/identities/ldap-auth/general.mdx +++ b/docs/documentation/platform/identities/ldap-auth/general.mdx @@ -5,6 +5,12 @@ description: "Learn how to authenticate with Infisical using LDAP." **LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory. +## Templates + +You can create reusable LDAP authentication templates to standardize configurations across multiple machine identities. Templates help ensure consistency, reduce configuration errors, and simplify identity management at scale. + +To create and manage LDAP auth templates, see our [Machine Identity Auth Templates documentation](/documentation/platform/identities/auth-templates). Once you've created a template, you can apply it when configuring LDAP auth for your identities in the guide below. + ## Guide diff --git a/docs/images/platform/identities/auth-templates/attach-template-form.png b/docs/images/platform/identities/auth-templates/attach-template-form.png new file mode 100644 index 000000000..7ac01d797 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/attach-template-form.png differ diff --git a/docs/images/platform/identities/auth-templates/machine-identity-page.png b/docs/images/platform/identities/auth-templates/machine-identity-page.png new file mode 100644 index 000000000..4574afa3f Binary files /dev/null and b/docs/images/platform/identities/auth-templates/machine-identity-page.png differ diff --git a/docs/images/platform/identities/auth-templates/template-usages-modal.png b/docs/images/platform/identities/auth-templates/template-usages-modal.png index 1048593b0..7f53c7a45 100644 Binary files a/docs/images/platform/identities/auth-templates/template-usages-modal.png and b/docs/images/platform/identities/auth-templates/template-usages-modal.png differ diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx index 80c843851..1395db5b8 100644 --- a/docs/internals/permissions/organization-permissions.mdx +++ b/docs/internals/permissions/organization-permissions.mdx @@ -217,3 +217,14 @@ Supports conditions and permission inversion | `edit-gateways` | Modify existing gateway settings | | `delete-gateways` | Remove gateways from organization | | `attach-gateways` | Attach gateways to resources | + +#### Subject: `machine-identity-auth-template` + +| Action | Description | +| ------------------ | ---------------------------------------------- | +| `list-templates` | View identity auth templates | +| `create-templates` | Create new identity auth templates | +| `edit-templates` | Modify existing identity auth templates | +| `delete-templates` | Remove identity auth templates | +| `unlink-templates` | Unlink identity auth templates from identities | +| `use-templates` | Attach identity auth templates to identities | diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 95d65273b..ece704f12 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -26,7 +26,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions { CreateTemplates = "create-templates", EditTemplates = "edit-templates", DeleteTemplates = "delete-templates", - UnlinkTemplates = "unlink-templates" + UnlinkTemplates = "unlink-templates", + UseTemplates = "use-templates" } export enum OrgPermissionSubjects { diff --git a/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx b/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx index 690cb4de7..8c05f1e46 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx +++ b/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx @@ -18,7 +18,7 @@ export const useCreateIdentityAuthTemplate = () => { return useMutation({ mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => { const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>( - "/api/v1/identities/templates", + "/api/v1/identity-templates", dto ); return data.template; @@ -37,7 +37,7 @@ export const useUpdateIdentityAuthTemplate = () => { return useMutation({ mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => { const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>( - `/api/v1/identities/templates/${dto.templateId}`, + `/api/v1/identity-templates/${dto.templateId}`, dto ); return data.template; @@ -58,7 +58,7 @@ export const useDeleteIdentityAuthTemplate = () => { return useMutation({ mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => { - await apiRequest.delete(`/api/v1/identities/templates/${dto.templateId}`, { + await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, { params: { organizationId: dto.organizationId } }); }, @@ -79,7 +79,7 @@ export const useUnlinkTemplateUsage = () => { return useMutation({ mutationFn: async (dto: UnlinkTemplateUsageDTO) => { const { data } = await apiRequest.post( - `/api/v1/identities/templates/${dto.templateId}/usage`, + `/api/v1/identity-templates/${dto.templateId}/delete-usage`, { identityIds: dto.identityIds }, { params: { organizationId: dto.organizationId } } ); diff --git a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx index de6f9b22e..a9981c0c4 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx +++ b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx @@ -29,7 +29,7 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => const { data } = await apiRequest.get<{ templates: IdentityAuthTemplate[]; totalCount: number; - }>("/api/v1/identities/templates/search", { + }>("/api/v1/identity-templates/search", { params: { organizationId: dto.organizationId, limit: dto.limit || 50, @@ -47,13 +47,13 @@ export const useGetIdentityAuthTemplate = (templateId: string, organizationId: s return useQuery({ queryKey: identityAuthTemplatesKeys.getTemplate(templateId), queryFn: async () => { - const { data } = await apiRequest.get<{ template: IdentityAuthTemplate }>( - `/api/v1/identities/templates/${templateId}`, + const { data } = await apiRequest.get( + `/api/v1/identity-templates/${templateId}`, { params: { organizationId } } ); - return data.template; + return data; }, enabled: Boolean(templateId) && Boolean(organizationId) }); @@ -63,12 +63,9 @@ export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) return useQuery({ queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod), queryFn: async () => { - const { data } = await apiRequest.get( - "/api/v1/identities/templates", - { - params: { authMethod } - } - ); + const { data } = await apiRequest.get("/api/v1/identity-templates", { + params: { authMethod } + }); return data; }, enabled: Boolean(authMethod) @@ -80,7 +77,7 @@ export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => { queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId), queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/identities/templates/${dto.templateId}/usage`, + `/api/v1/identity-templates/${dto.templateId}/usage`, { params: { organizationId: dto.organizationId } } diff --git a/frontend/src/hooks/api/identityAuthTemplates/types.ts b/frontend/src/hooks/api/identityAuthTemplates/types.ts index 07a1eb018..860f9a9fc 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/types.ts +++ b/frontend/src/hooks/api/identityAuthTemplates/types.ts @@ -7,6 +7,7 @@ export interface LdapTemplateFields { bindDN: string; bindPass: string; searchBase: string; + ldapCaCertificate?: string; } export interface IdentityAuthTemplate { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx index 1c19ca122..3bd423982 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx @@ -11,7 +11,8 @@ import { Modal, ModalContent, Select, - SelectItem + SelectItem, + TextArea } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { @@ -29,7 +30,11 @@ const schema = z.object({ url: z.string().min(1, "LDAP URL is required"), bindDN: z.string().min(1, "Bind DN is required"), bindPass: z.string().min(1, "Bind Pass is required"), - searchBase: z.string().min(1, "Search Base / DN is required") + searchBase: z.string().min(1, "Search Base / DN is required"), + ldapCaCertificate: z + .string() + .optional() + .transform((val) => val || undefined) }); export type FormData = z.infer; @@ -66,7 +71,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = url: "", bindDN: "", bindPass: "", - searchBase: "" + searchBase: "", + ldapCaCertificate: "" } }); @@ -78,7 +84,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = url: template.templateFields?.url || "", bindDN: template.templateFields?.bindDN || "", bindPass: template.templateFields?.bindPass || "", - searchBase: template.templateFields?.searchBase || "" + searchBase: template.templateFields?.searchBase || "", + ldapCaCertificate: template.templateFields?.ldapCaCertificate || "" }); } else { reset({ @@ -87,7 +94,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = url: "", bindDN: "", bindPass: "", - searchBase: "" + searchBase: "", + ldapCaCertificate: "" }); } }, [isEdit, template, reset]); @@ -105,7 +113,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = url: data.url, bindDN: data.bindDN, bindPass: data.bindPass, - searchBase: data.searchBase + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate } }); createNotification({ @@ -121,7 +130,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = url: data.url, bindDN: data.bindDN, bindPass: data.bindPass, - searchBase: data.searchBase + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate } }); createNotification({ @@ -267,6 +277,22 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = )} /> + + ( + +