General improvements on auth templates

This commit is contained in:
Carlos Monastyrski
2025-08-04 15:29:07 -03:00
parent ebe05661d3
commit 3dde786621
29 changed files with 598 additions and 557 deletions
@@ -1352,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>; accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[]; allowedFields?: TAllowedFields[];
url: string; url: string;
templateId?: string | null;
}; };
} }
@@ -1365,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>; accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[]; allowedFields?: TAllowedFields[];
url?: string; url?: string;
templateId?: string | null;
}; };
} }
@@ -33,7 +33,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
EditTemplates = "edit-templates", EditTemplates = "edit-templates",
CreateTemplates = "create-templates", CreateTemplates = "create-templates",
DeleteTemplates = "delete-templates", DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates" UnlinkTemplates = "unlink-templates",
UseTemplates = "use-templates"
} }
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
@@ -382,6 +383,7 @@ const buildAdminPermission = () => {
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
); );
can(OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare); can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
@@ -423,6 +425,7 @@ const buildMemberPermission = () => {
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
); );
can(OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
return rules; return rules;
}; };
+2 -38
View File
@@ -148,42 +148,6 @@ export const IDENTITIES = {
} }
} as const; } 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 = { export const UNIVERSAL_AUTH = {
LOGIN: { LOGIN: {
clientId: "Your Machine Identity Client ID.", clientId: "Your Machine Identity Client ID.",
@@ -251,7 +215,7 @@ export const LDAP_AUTH = {
password: "The password of the LDAP user to login." password: "The password of the LDAP user to login."
}, },
ATTACH: { 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.", identityId: "The ID of the identity to attach the configuration onto.",
url: "The URL of the LDAP server.", url: "The URL of the LDAP server.",
allowedFields: allowedFields:
@@ -279,7 +243,7 @@ export const LDAP_AUTH = {
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", 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.", 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: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for." identityId: "The ID of the identity to retrieve the configuration for."
+2 -1
View File
@@ -1469,7 +1469,8 @@ export const registerRoutes = async (
identityLdapAuthDAL, identityLdapAuthDAL,
permissionService, permissionService,
kmsService, kmsService,
licenseService licenseService,
auditLogService
}); });
const identityAccessTokenService = identityAccessTokenServiceFactory({ const identityAccessTokenService = identityAccessTokenServiceFactory({
@@ -200,55 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
params: z.object({ params: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId) identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
}), }),
body: z body: z.union([
.object({ // Template-based configuration
templateId: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.templateId), z
url: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.url), .object({
bindDN: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.bindDN), templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId),
bindPass: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.bindPass), searchFilter: z
searchBase: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.searchBase), .string()
searchFilter: z .trim()
.string() .min(1)
.trim() .default("(uid={{username}})")
.min(1) .refine(isValidLdapFilter, "Invalid LDAP search filter")
.default("(uid={{username}})") .describe(LDAP_AUTH.ATTACH.searchFilter),
.refine(isValidLdapFilter, "Invalid LDAP search filter") allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
.describe(LDAP_AUTH.ATTACH.searchFilter), ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), accessTokenTrustedIps: z
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), .object({
accessTokenTrustedIps: z ipAddress: z.string().trim()
.object({ })
ipAddress: z.string().trim() .array()
}) .min(1)
.array() .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.min(1) .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) accessTokenTTL: z
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), .number()
accessTokenTTL: z .int()
.number() .min(0)
.int() .max(315360000)
.min(0) .default(2592000)
.max(315360000) .describe(LDAP_AUTH.ATTACH.accessTokenTTL),
.default(2592000) accessTokenMaxTTL: z
.describe(LDAP_AUTH.ATTACH.accessTokenTTL), .number()
accessTokenMaxTTL: z .int()
.number() .min(1)
.int() .max(315360000)
.min(1) .default(2592000)
.max(315360000) .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
.default(2592000) accessTokenNumUsesLimit: z
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), .number()
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) .int()
}) .min(0)
.refine( .default(0)
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL, .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
"Access Token TTL cannot be greater than Access Token Max TTL." })
) .refine(
.refine((val) => { (val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
const hasTemplateId = !!val.templateId; "Access Token TTL cannot be greater than Access Token Max TTL."
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."), // 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: { response: {
200: z.object({ 200: z.object({
identityLdapAuth: IdentityLdapAuthsSchema.omit({ identityLdapAuth: IdentityLdapAuthsSchema.omit({
@@ -281,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, 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, accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
allowedFields: req.body.allowedFields allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
} }
} }
}); });
@@ -12,10 +12,11 @@ import {
} from "@app/services/identity-auth-template/identity-auth-template-enums"; } from "@app/services/identity-auth-template/identity-auth-template-enums";
const ldapTemplateFieldsSchema = z.object({ const ldapTemplateFieldsSchema = z.object({
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP_URL_REQUIRED), url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.BIND_DN_REQUIRED), bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.BIND_PASSWORD_REQUIRED), bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.SEARCH_BASE_REQUIRED) searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
ldapCaCertificate: z.string().trim().optional()
}); });
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => { export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
@@ -44,8 +45,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
templateFields: ldapTemplateFieldsSchema templateFields: ldapTemplateFieldsSchema
}), }),
response: { response: {
200: z.object({ 200: IdentityAuthTemplatesSchema.extend({
message: z.string() 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() templateFields: ldapTemplateFieldsSchema.partial().optional()
}), }),
response: { response: {
200: z.object({ 200: IdentityAuthTemplatesSchema.extend({
message: z.string() 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) templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}), }),
response: { response: {
200: z.object({ 200: IdentityAuthTemplatesSchema.extend({
template: IdentityAuthTemplatesSchema.extend({ templateFields: ldapTemplateFieldsSchema
templateFields: ldapTemplateFieldsSchema
})
}) })
} }
}, },
@@ -220,7 +219,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
return { template }; return template;
} }
}); });
@@ -347,7 +346,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
server.route({ server.route({
method: "POST", method: "POST",
url: "/:templateId/usage", url: "/:templateId/delete-usage",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
+2 -7
View File
@@ -135,13 +135,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIntegrationRouter, { prefix: "/integration" }); await server.register(registerIntegrationRouter, { prefix: "/integration" });
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" }); await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
await server.register(registerWebhookRouter, { prefix: "/webhooks" }); await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register( await server.register(registerIdentityRouter, { prefix: "/identities" });
async (identitiesRouter) => { await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
await identitiesRouter.register(registerIdentityRouter);
await identitiesRouter.register(registerIdentityTemplateRouter, { prefix: "/templates" });
},
{ prefix: "/identities" }
);
await server.register( await server.register(
async (secretSharingRouter) => { async (secretSharingRouter) => {
@@ -7,10 +7,12 @@ export const TEMPLATE_VALIDATION_MESSAGES = {
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long", TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
AUTH_METHOD_REQUIRED: "Auth method is required", AUTH_METHOD_REQUIRED: "Auth method is required",
TEMPLATE_ID_REQUIRED: "Template ID is required", TEMPLATE_ID_REQUIRED: "Template ID is required",
LDAP_URL_REQUIRED: "LDAP URL is required", LDAP: {
BIND_DN_REQUIRED: "Bind DN is required", URL_REQUIRED: "LDAP URL is required",
BIND_PASSWORD_REQUIRED: "Bind password is required", BIND_DN_REQUIRED: "Bind DN is required",
SEARCH_BASE_REQUIRED: "Search base is required" BIND_PASSWORD_REQUIRED: "Bind password is required",
SEARCH_BASE_REQUIRED: "Search base is required"
}
} as const; } as const;
export const TEMPLATE_SUCCESS_MESSAGES = { export const TEMPLATE_SUCCESS_MESSAGES = {
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { import {
OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionMachineIdentityAuthTemplateActions,
@@ -9,6 +10,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityLdapAuthDALFactory } from "../identity-ldap-auth/identity-ldap-auth-dal"; import { TIdentityLdapAuthDALFactory } from "../identity-ldap-auth/identity-ldap-auth-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; import { KmsDataKey } from "../kms/kms-types";
@@ -30,6 +32,7 @@ type TIdentityAuthTemplateServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
}; };
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>; export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
@@ -39,7 +42,8 @@ export const identityAuthTemplateServiceFactory = ({
identityLdapAuthDAL, identityLdapAuthDAL,
permissionService, permissionService,
kmsService, kmsService,
licenseService licenseService,
auditLogService
}: TIdentityAuthTemplateServiceFactoryDep) => { }: TIdentityAuthTemplateServiceFactoryDep) => {
// Plan check // Plan check
const $checkPlan = async (orgId: string) => { const $checkPlan = async (orgId: string) => {
@@ -87,7 +91,7 @@ export const identityAuthTemplateServiceFactory = ({
orgId: actorOrgId orgId: actorOrgId
}); });
return template; return { ...template, templateFields };
}; };
const updateTemplate = async ({ const updateTemplate = async ({
@@ -104,7 +108,7 @@ export const identityAuthTemplateServiceFactory = ({
templateFields?: Record<string, unknown>; templateFields?: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => { } & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findById(templateId); const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) { if (!template) {
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
@@ -126,6 +130,8 @@ export const identityAuthTemplateServiceFactory = ({
orgId: template.orgId orgId: template.orgId
}); });
let finalTemplateFields: Record<string, unknown> = {};
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => { const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
const authTemplate = await identityAuthTemplateDAL.updateById( const authTemplate = await identityAuthTemplateDAL.updateById(
templateId, templateId,
@@ -149,12 +155,13 @@ export const identityAuthTemplateServiceFactory = ({
) as TLdapTemplateFields; ) as TLdapTemplateFields;
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields }; const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
finalTemplateFields = mergedTemplateFields;
const ldapUpdateData: { const ldapUpdateData: {
url?: string; url?: string;
searchBase?: string; searchBase?: string;
encryptedBindDN?: Buffer; encryptedBindDN?: Buffer;
encryptedBindPass?: Buffer; encryptedBindPass?: Buffer;
encryptedLdapCaCertificate?: Buffer;
} = {}; } = {};
if ("url" in templateFields) { if ("url" in templateFields) {
@@ -173,16 +180,38 @@ export const identityAuthTemplateServiceFactory = ({
plainText: Buffer.from(mergedTemplateFields.bindPass) plainText: Buffer.from(mergedTemplateFields.bindPass)
}).cipherTextBlob; }).cipherTextBlob;
} }
if ("ldapCaCertificate" in templateFields) {
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
}).cipherTextBlob;
}
if (Object.keys(ldapUpdateData).length > 0) { 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 authTemplate;
}); });
return updatedTemplate; return { ...updatedTemplate, templateFields: finalTemplateFields };
}; };
const deleteTemplate = async ({ const deleteTemplate = async ({
@@ -193,7 +222,7 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
}: TDeleteIdentityAuthTemplateDTO) => { }: TDeleteIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findById(templateId); const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) { if (!template) {
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
@@ -212,7 +241,25 @@ export const identityAuthTemplateServiceFactory = ({
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => { const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
// Remove template reference from identityLdapAuth records // 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 // Delete the template
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx); const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
@@ -230,7 +277,7 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
}: TGetIdentityAuthTemplateDTO) => { }: TGetIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findById(templateId); const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) { if (!template) {
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
@@ -313,7 +360,7 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
); );
@@ -350,7 +397,7 @@ export const identityAuthTemplateServiceFactory = ({
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
); );
const template = await identityAuthTemplateDAL.findById(templateId); const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) { if (!template) {
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
@@ -376,11 +423,11 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
); );
const template = await identityAuthTemplateDAL.findById(templateId); const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) { if (!template) {
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
@@ -8,6 +8,7 @@ export type TLdapTemplateFields = {
bindDN: string; bindDN: string;
bindPass: string; bindPass: string;
searchBase: string; searchBase: string;
ldapCaCertificate?: string;
}; };
// Union type for all template field types // Union type for all template field types
@@ -4,7 +4,11 @@ import { ForbiddenError } from "@casl/ability";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; 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 { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@@ -217,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); 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); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
if (!plan.ldap) { if (!plan.ldap) {
@@ -254,7 +266,7 @@ export const identityLdapAuthServiceFactory = ({
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
: undefined; : 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) { if (template) {
ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString()); ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
} else { } else {
@@ -267,7 +279,8 @@ export const identityLdapAuthServiceFactory = ({
bindDN, bindDN,
bindPass, bindPass,
searchBase, searchBase,
url url,
ldapCaCertificate
}; };
} }
@@ -280,9 +293,9 @@ export const identityLdapAuthServiceFactory = ({
}); });
let encryptedLdapCaCertificate: Buffer | undefined; let encryptedLdapCaCertificate: Buffer | undefined;
if (ldapCaCertificate) { if (ldapConfig.ldapCaCertificate) {
const { cipherTextBlob: encryptedCertificate } = encryptor({ const { cipherTextBlob: encryptedCertificate } = encryptor({
plainText: Buffer.from(ldapCaCertificate) plainText: Buffer.from(ldapConfig.ldapCaCertificate)
}); });
encryptedLdapCaCertificate = encryptedCertificate; encryptedLdapCaCertificate = encryptedCertificate;
@@ -291,7 +304,7 @@ export const identityLdapAuthServiceFactory = ({
const isConnected = await testLDAPConfig({ const isConnected = await testLDAPConfig({
bindDN: ldapConfig.bindDN, bindDN: ldapConfig.bindDN,
bindPass: ldapConfig.bindPass, bindPass: ldapConfig.bindPass,
caCert: ldapCaCertificate || "", caCert: ldapConfig.ldapCaCertificate || "",
url: ldapConfig.url url: ldapConfig.url
}); });
@@ -371,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); 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); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
if (!plan.ldap) { if (!plan.ldap) {
@@ -411,6 +431,7 @@ export const identityLdapAuthServiceFactory = ({
bindPass?: string; bindPass?: string;
searchBase?: string; searchBase?: string;
url?: string; url?: string;
ldapCaCertificate?: string;
}; };
if (template) { if (template) {
@@ -420,7 +441,8 @@ export const identityLdapAuthServiceFactory = ({
bindDN, bindDN,
bindPass, bindPass,
searchBase, searchBase,
url url,
ldapCaCertificate
}; };
} }
@@ -434,9 +456,9 @@ export const identityLdapAuthServiceFactory = ({
} }
let encryptedLdapCaCertificate: Buffer | undefined; let encryptedLdapCaCertificate: Buffer | undefined;
if (ldapCaCertificate) { if (config.ldapCaCertificate) {
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({ const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
plainText: Buffer.from(ldapCaCertificate) plainText: Buffer.from(config.ldapCaCertificate)
}); });
encryptedLdapCaCertificate = ldapCaCertificateCiphertext; encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
@@ -456,7 +478,7 @@ export const identityLdapAuthServiceFactory = ({
const isConnected = await testLDAPConfig({ const isConnected = await testLDAPConfig({
bindDN: config.bindDN || ldapConfig.bindDN, bindDN: config.bindDN || ldapConfig.bindDN,
bindPass: config.bindPass || ldapConfig.bindPass, bindPass: config.bindPass || ldapConfig.bindPass,
caCert: ldapCaCertificate || ldapConfig.caCert, caCert: config.ldapCaCertificate || ldapConfig.caCert,
url: config.url || ldapConfig.url url: config.url || ldapConfig.url
}); });
+1 -1
View File
@@ -291,7 +291,6 @@
{ {
"group": "Machine Identities", "group": "Machine Identities",
"pages": [ "pages": [
"documentation/platform/identities/auth-templates",
"documentation/platform/identities/alicloud-auth", "documentation/platform/identities/alicloud-auth",
"documentation/platform/identities/aws-auth", "documentation/platform/identities/aws-auth",
"documentation/platform/identities/azure-auth", "documentation/platform/identities/azure-auth",
@@ -323,6 +322,7 @@
} }
] ]
}, },
"documentation/platform/identities/auth-templates",
"documentation/platform/token", "documentation/platform/token",
"documentation/platform/mfa", "documentation/platform/mfa",
"documentation/platform/github-org-sync" "documentation/platform/github-org-sync"
@@ -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. 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 ### 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](/images/platform/identities/auth-templates/template-usages.png)
![Template Usages Modal](/images/platform/identities/auth-templates/template-usages-modal.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 ## FAQ
<AccordionGroup> <AccordionGroup>
@@ -85,11 +85,11 @@ You can view which identities are using a specific template by clicking **View U
</Accordion> </Accordion>
<Accordion title="What happens if I delete a template that's in use?"> <Accordion title="What happens if I delete a template that's in use?">
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.
</Accordion> </Accordion>
<Accordion title="Can I see which identities are using a specific template?"> <Accordion title="Can I see which identities are using a specific template?">
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.
</Accordion> </Accordion>
<Accordion title="Do templates support all authentication methods?"> <Accordion title="Do templates support all authentication methods?">
@@ -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. **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 ## Guide
<Steps> <Steps>
<Step title="Creating an identity"> <Step title="Creating an identity">
Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

After

Width:  |  Height:  |  Size: 192 KiB

@@ -217,3 +217,14 @@ Supports conditions and permission inversion
| `edit-gateways` | Modify existing gateway settings | | `edit-gateways` | Modify existing gateway settings |
| `delete-gateways` | Remove gateways from organization | | `delete-gateways` | Remove gateways from organization |
| `attach-gateways` | Attach gateways to resources | | `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 |
@@ -26,7 +26,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
CreateTemplates = "create-templates", CreateTemplates = "create-templates",
EditTemplates = "edit-templates", EditTemplates = "edit-templates",
DeleteTemplates = "delete-templates", DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates" UnlinkTemplates = "unlink-templates",
UseTemplates = "use-templates"
} }
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
@@ -18,7 +18,7 @@ export const useCreateIdentityAuthTemplate = () => {
return useMutation({ return useMutation({
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => { mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>( const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
"/api/v1/identities/templates", "/api/v1/identity-templates",
dto dto
); );
return data.template; return data.template;
@@ -37,7 +37,7 @@ export const useUpdateIdentityAuthTemplate = () => {
return useMutation({ return useMutation({
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => { mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>( const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
`/api/v1/identities/templates/${dto.templateId}`, `/api/v1/identity-templates/${dto.templateId}`,
dto dto
); );
return data.template; return data.template;
@@ -58,7 +58,7 @@ export const useDeleteIdentityAuthTemplate = () => {
return useMutation({ return useMutation({
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => { 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 } params: { organizationId: dto.organizationId }
}); });
}, },
@@ -79,7 +79,7 @@ export const useUnlinkTemplateUsage = () => {
return useMutation({ return useMutation({
mutationFn: async (dto: UnlinkTemplateUsageDTO) => { mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>( const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
`/api/v1/identities/templates/${dto.templateId}/usage`, `/api/v1/identity-templates/${dto.templateId}/delete-usage`,
{ identityIds: dto.identityIds }, { identityIds: dto.identityIds },
{ params: { organizationId: dto.organizationId } } { params: { organizationId: dto.organizationId } }
); );
@@ -29,7 +29,7 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) =>
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{
templates: IdentityAuthTemplate[]; templates: IdentityAuthTemplate[];
totalCount: number; totalCount: number;
}>("/api/v1/identities/templates/search", { }>("/api/v1/identity-templates/search", {
params: { params: {
organizationId: dto.organizationId, organizationId: dto.organizationId,
limit: dto.limit || 50, limit: dto.limit || 50,
@@ -47,13 +47,13 @@ export const useGetIdentityAuthTemplate = (templateId: string, organizationId: s
return useQuery({ return useQuery({
queryKey: identityAuthTemplatesKeys.getTemplate(templateId), queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ template: IdentityAuthTemplate }>( const { data } = await apiRequest.get<IdentityAuthTemplate>(
`/api/v1/identities/templates/${templateId}`, `/api/v1/identity-templates/${templateId}`,
{ {
params: { organizationId } params: { organizationId }
} }
); );
return data.template; return data;
}, },
enabled: Boolean(templateId) && Boolean(organizationId) enabled: Boolean(templateId) && Boolean(organizationId)
}); });
@@ -63,12 +63,9 @@ export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod)
return useQuery({ return useQuery({
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod), queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<IdentityAuthTemplate[]>( const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/api/v1/identity-templates", {
"/api/v1/identities/templates", params: { authMethod }
{ });
params: { authMethod }
}
);
return data; return data;
}, },
enabled: Boolean(authMethod) enabled: Boolean(authMethod)
@@ -80,7 +77,7 @@ export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => {
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId), queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>( const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
`/api/v1/identities/templates/${dto.templateId}/usage`, `/api/v1/identity-templates/${dto.templateId}/usage`,
{ {
params: { organizationId: dto.organizationId } params: { organizationId: dto.organizationId }
} }
@@ -7,6 +7,7 @@ export interface LdapTemplateFields {
bindDN: string; bindDN: string;
bindPass: string; bindPass: string;
searchBase: string; searchBase: string;
ldapCaCertificate?: string;
} }
export interface IdentityAuthTemplate { export interface IdentityAuthTemplate {
@@ -11,7 +11,8 @@ import {
Modal, Modal,
ModalContent, ModalContent,
Select, Select,
SelectItem SelectItem,
TextArea
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import {
@@ -29,7 +30,11 @@ const schema = z.object({
url: z.string().min(1, "LDAP URL is required"), url: z.string().min(1, "LDAP URL is required"),
bindDN: z.string().min(1, "Bind DN is required"), bindDN: z.string().min(1, "Bind DN is required"),
bindPass: z.string().min(1, "Bind Pass 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<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -66,7 +71,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: "", url: "",
bindDN: "", bindDN: "",
bindPass: "", bindPass: "",
searchBase: "" searchBase: "",
ldapCaCertificate: ""
} }
}); });
@@ -78,7 +84,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: template.templateFields?.url || "", url: template.templateFields?.url || "",
bindDN: template.templateFields?.bindDN || "", bindDN: template.templateFields?.bindDN || "",
bindPass: template.templateFields?.bindPass || "", bindPass: template.templateFields?.bindPass || "",
searchBase: template.templateFields?.searchBase || "" searchBase: template.templateFields?.searchBase || "",
ldapCaCertificate: template.templateFields?.ldapCaCertificate || ""
}); });
} else { } else {
reset({ reset({
@@ -87,7 +94,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: "", url: "",
bindDN: "", bindDN: "",
bindPass: "", bindPass: "",
searchBase: "" searchBase: "",
ldapCaCertificate: ""
}); });
} }
}, [isEdit, template, reset]); }, [isEdit, template, reset]);
@@ -105,7 +113,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: data.url, url: data.url,
bindDN: data.bindDN, bindDN: data.bindDN,
bindPass: data.bindPass, bindPass: data.bindPass,
searchBase: data.searchBase searchBase: data.searchBase,
ldapCaCertificate: data.ldapCaCertificate
} }
}); });
createNotification({ createNotification({
@@ -121,7 +130,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: data.url, url: data.url,
bindDN: data.bindDN, bindDN: data.bindDN,
bindPass: data.bindPass, bindPass: data.bindPass,
searchBase: data.searchBase searchBase: data.searchBase,
ldapCaCertificate: data.ldapCaCertificate
} }
}); });
createNotification({ createNotification({
@@ -267,6 +277,22 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="ldapCaCertificate"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
isOptional
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</> </>
)} )}
@@ -182,7 +182,7 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
<Td>{template.name}</Td> <Td>{template.name}</Td>
<Td> <Td>
<div className="flex items-center"> <div className="flex items-center">
<span className="capitalize">{template.authMethod}</span> <span className="uppercase">{template.authMethod}</span>
</div> </div>
</Td> </Td>
<Td> <Td>
@@ -20,7 +20,11 @@ import {
TextArea, TextArea,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { import {
MachineIdentityAuthMethod, MachineIdentityAuthMethod,
useAddIdentityLdapAuth, useAddIdentityLdapAuth,
@@ -143,6 +147,12 @@ export const IdentityLdapAuthForm = ({
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration); const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP); const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
const { permission } = useOrgPermission();
const canUseTemplates = permission.can(
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { data } = useGetIdentityLdapAuth(identityId ?? "", { const { data } = useGetIdentityLdapAuth(identityId ?? "", {
enabled: isUpdate enabled: isUpdate
@@ -153,11 +163,12 @@ export const IdentityLdapAuthForm = ({
handleSubmit, handleSubmit,
reset, reset,
watch, watch,
setValue,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
scope: "template", scope: "custom",
templateId: "", templateId: "",
url: "", url: "",
bindDN: "", bindDN: "",
@@ -188,15 +199,11 @@ export const IdentityLdapAuthForm = ({
// Helper function to determine scope based on existing data // Helper function to determine scope based on existing data
const determineScope = (authData: any) => { const determineScope = (authData: any) => {
// If templateId exists in the data, it's template scope // If templateId exists in the data, it's template scope
// If url, bindDN, bindPass, searchBase exist, it's custom scope
if (authData.templateId) { if (authData.templateId) {
return "template"; return "template";
} }
if (authData.url || authData.bindDN || authData.bindPass || authData.searchBase) { // Default to custom if we can't determine
return "custom"; return "custom";
}
// Default to template if we can't determine
return "template";
}; };
useEffect(() => { useEffect(() => {
@@ -228,7 +235,7 @@ export const IdentityLdapAuthForm = ({
} }
reset({ reset({
scope: "template", scope: "custom",
templateId: "", templateId: "",
url: "", url: "",
bindDN: "", bindDN: "",
@@ -345,30 +352,38 @@ export const IdentityLdapAuthForm = ({
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> <Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList> </TabList>
<TabPanel value={IdentityFormTab.Configuration}> <TabPanel value={IdentityFormTab.Configuration}>
<Controller {canUseTemplates && (
control={control} <Controller
name="scope" control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( name="scope"
<FormControl render={({ field: { value, onChange }, fieldState: { error } }) => (
label="Configuration Type" <FormControl
isError={Boolean(error)} label="Configuration Type"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Select
value={value}
onValueChange={(val) => {
onChange(val);
}}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<SelectItem value="template">Use Template</SelectItem> <Select
<SelectItem value="custom">Custom Configuration</SelectItem> value={value}
</Select> onValueChange={(val) => {
</FormControl> onChange(val);
)} setValue("templateId", data?.templateId || "");
/> setValue("url", data?.url || "");
setValue("bindDN", data?.bindDN || "");
setValue("bindPass", data?.bindPass || "");
setValue("searchBase", data?.searchBase || "");
setValue("ldapCaCertificate", data?.ldapCaCertificate || "");
}}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value="template">Use Template</SelectItem>
<SelectItem value="custom">Custom Configuration</SelectItem>
</Select>
</FormControl>
)}
/>
)}
{scope === "template" && ( {scope === "template" && (
<Controller <Controller
@@ -385,6 +400,13 @@ export const IdentityLdapAuthForm = ({
value={value} value={value}
onValueChange={(val) => { onValueChange={(val) => {
onChange(val); onChange(val);
const tmp = templates?.find((t) => t.id === val);
if (!tmp) return;
setValue("url", tmp.templateFields.url);
setValue("bindDN", tmp.templateFields.bindDN);
setValue("bindPass", tmp.templateFields.bindPass);
setValue("searchBase", tmp.templateFields.searchBase);
setValue("ldapCaCertificate", tmp.templateFields.ldapCaCertificate);
}} }}
className="w-full" className="w-full"
position="popper" position="popper"
@@ -404,66 +426,104 @@ export const IdentityLdapAuthForm = ({
/> />
)} )}
{scope === "custom" && ( <Controller
<> control={control}
<Controller name="url"
control={control} render={({ field, fieldState: { error } }) => (
name="url" <FormControl
render={({ field, fieldState: { error } }) => ( label="LDAP URL"
<FormControl isError={Boolean(error)}
label="LDAP URL" errorText={error?.message}
isError={Boolean(error)} tooltipText={
errorText={error?.message} scope === "template"
isRequired ? "This field cannot be modified when using a template"
> : undefined
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" /> }
</FormControl> isRequired
)} >
/> <Input
<Controller {...field}
control={control} placeholder="ldaps://domain-or-ip:636"
name="bindDN" type="text"
render={({ field, fieldState: { error } }) => ( isDisabled={scope === "template"}
<FormControl containerClassName={scope === "template" ? "opacity-55" : ""}
isRequired />
label="Bind DN" </FormControl>
isError={Boolean(error)} )}
errorText={error?.message} />
> <Controller
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" /> control={control}
</FormControl> name="bindDN"
)} render={({ field, fieldState: { error } }) => (
/> <FormControl
<Controller isRequired
control={control} label="Bind DN"
name="bindPass" isError={Boolean(error)}
render={({ field, fieldState: { error } }) => ( errorText={error?.message}
<FormControl tooltipText={
isRequired scope === "template"
label="Bind Pass" ? "This field cannot be modified when using a template"
isError={Boolean(error)} : undefined
errorText={error?.message} }
> >
<Input {...field} placeholder="********" type="password" /> <Input
</FormControl> {...field}
)} containerClassName={scope === "template" ? "opacity-55" : ""}
/> placeholder="cn=infisical,ou=Users,dc=example,dc=com"
<Controller isDisabled={scope === "template"}
control={control} />
name="searchBase" </FormControl>
render={({ field, fieldState: { error } }) => ( )}
<FormControl />
isRequired <Controller
label="Search Base / DN" control={control}
isError={Boolean(error)} name="bindPass"
errorText={error?.message} render={({ field, fieldState: { error } }) => (
> <FormControl
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" /> isRequired
</FormControl> label="Bind Pass"
)} isError={Boolean(error)}
/> errorText={error?.message}
</> tooltipText={
)} scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input
{...field}
placeholder="********"
type="password"
containerClassName={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Search Base / DN"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input
{...field}
placeholder="ou=machines,dc=acme,dc=com"
containerClassName={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
@@ -641,9 +701,18 @@ export const IdentityLdapAuthForm = ({
isOptional isOptional
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server." tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: "An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
}
> >
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." /> <TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
className={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl> </FormControl>
)} )}
/> />
@@ -103,93 +103,93 @@ export const IdentitySection = withPermission(
}; };
return ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div>
<div className="mb-4 flex items-center justify-between"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center gap-1"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p> <div className="flex items-center gap-1">
<a <p className="text-xl font-semibold text-mineshaft-100">Identities</p>
href="https://infisical.com/docs/documentation/platform/identities/overview" <a
target="_blank" href="https://infisical.com/docs/documentation/platform/identities/overview"
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description: "You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
> >
Create Identity <div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
</Button> <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
)} <span>Docs</span>
</OrgPermissionCan> <FontAwesomeIcon
</div> icon={faArrowUpRightFromSquare}
<IdentityTable handlePopUpOpen={handlePopUpOpen} /> className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
{/* Identity Auth Templates Section */} </div>
{subscription.machineIdentityAuthTemplates && ( </a>
<div className="mt-8">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identity Auth Templates</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createTemplate")}
isDisabled={!isAllowed}
>
Create Template
</Button>
)}
</OrgPermissionCan>
</div> </div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} /> <OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description:
"You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
>
Create Identity
</Button>
)}
</OrgPermissionCan>
</div> </div>
)} <IdentityTable handlePopUpOpen={handlePopUpOpen} />
</div>
{/* Identity Auth Templates Section */}
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identity Auth Templates</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createTemplate")}
isDisabled={!isAllowed}
>
Create Template
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
</div>
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<MachineAuthTemplateUsagesModal <MachineAuthTemplateUsagesModal
@@ -1,14 +1,6 @@
import { useEffect, useState } from "react"; import { faCertificate } from "@fortawesome/free-solid-svg-icons";
import { faCertificate, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { import {
Badge,
Button,
Checkbox,
DeleteActionModal,
EmptyState, EmptyState,
Modal, Modal,
ModalContent, ModalContent,
@@ -22,14 +14,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
MachineAuthTemplateUsage,
TEMPLATE_ERROR_MESSAGES,
TEMPLATE_UI_LABELS,
useGetTemplateUsages,
useUnlinkTemplateUsage
} from "@app/hooks/api/identityAuthTemplates";
import { usePopUp } from "@app/hooks/usePopUp";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@@ -45,201 +30,51 @@ export const MachineAuthTemplateUsagesModal = ({
templateName templateName
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const [selectedUsageIds, setSelectedUsageIds] = useState<string[]>([]);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"unlinkUsages"
] as const);
useEffect(() => {
if (!isOpen) {
setSelectedUsageIds([]);
handlePopUpClose("unlinkUsages");
}
}, [isOpen, handlePopUpClose]);
const organizationId = currentOrg?.id || ""; const organizationId = currentOrg?.id || "";
const { const { data: usages = [], isPending } = useGetTemplateUsages({
data: usages = [],
isPending,
refetch
} = useGetTemplateUsages({
templateId, templateId,
organizationId organizationId
}); });
const { mutateAsync: unlinkUsage } = useUnlinkTemplateUsage();
const handleUnlinkUsages = async (selectedUsages: MachineAuthTemplateUsage[]) => {
try {
await unlinkUsage({
templateId,
identityIds: selectedUsages.map((usage) => usage.identityId),
organizationId
});
createNotification({
text: TEMPLATE_ERROR_MESSAGES.UNLINK_SUCCESS,
type: "success"
});
setSelectedUsageIds([]);
handlePopUpClose("unlinkUsages");
refetch();
} catch {
createNotification({
text: TEMPLATE_ERROR_MESSAGES.UNLINK_FAILED,
type: "error"
});
}
};
const handleUsageToggle = (usageId: string) => {
setSelectedUsageIds((prev) =>
prev.includes(usageId) ? prev.filter((id) => id !== usageId) : [...prev, usageId]
);
};
const handleSelectAll = () => {
if (selectedUsageIds.length === usages.length) {
setSelectedUsageIds([]);
} else {
setSelectedUsageIds(usages.map((usage) => usage.identityId));
}
};
return ( return (
<> <Modal isOpen={isOpen} onOpenChange={onClose}>
<Modal isOpen={isOpen} onOpenChange={onClose}> <ModalContent title={`Usages for Identity Auth Template: ${templateName}`}>
<ModalContent <div>
title={`Auth Template Usages: ${templateName}`} <TableContainer>
subTitle="Manage identities using this template" <Table>
className="max-w-4xl" <THead>
> <Tr className="h-14">
<div> <Th>Identity Name</Th>
<div <Th>Identity ID</Th>
className={twMerge( </Tr>
"h-0 flex-shrink-0 overflow-hidden transition-all", </THead>
selectedUsageIds.length > 0 && "h-16" <TBody>
)} {isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
> {!isPending &&
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300"> usages.map((usage) => (
<div className="mr-2 text-sm">{selectedUsageIds.length} Selected</div> <Tr
<button className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
type="button" key={`usage-${usage.identityId}`}
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
onClick={() => setSelectedUsageIds([])}
>
{TEMPLATE_UI_LABELS.UNSELECT_ALL}
</button>
<Button
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
className="ml-2"
onClick={() => {
const selectedUsagesList = usages.filter((usage) =>
selectedUsageIds.includes(usage.identityId)
);
if (!selectedUsagesList.length) return;
handlePopUpOpen("unlinkUsages", { selectedUsagesList });
}}
size="xs"
>
{TEMPLATE_UI_LABELS.UNLINK}
</Button>
</div>
</div>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th className="w-12">
<Checkbox
id="select-all"
className="mr-2"
isChecked={usages.length > 0 && selectedUsageIds.length === usages.length}
onCheckedChange={handleSelectAll}
/>
</Th>
<Th>Identity Name</Th>
<Th>Identity ID</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
{!isPending &&
usages.map((usage) => (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`usage-${usage.identityId}`}
>
<Td>
<Checkbox
id={`select-usage-${usage.identityId}`}
isChecked={selectedUsageIds.includes(usage.identityId)}
onCheckedChange={() => handleUsageToggle(usage.identityId)}
/>
</Td>
<Td>{usage.identityName}</Td>
<Td>
<span className="text-sm text-mineshaft-400">{usage.identityId}</span>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isPending && usages.length === 0 && (
<EmptyState
title="This template is not currently being used by any identities"
icon={faCertificate}
/>
)}
</TableContainer>
</div>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.unlinkUsages.isOpen}
title="Are you sure you want to unlink the following template usages?"
onChange={(isDeleteOpen) => handlePopUpToggle("unlinkUsages", isDeleteOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
handleUnlinkUsages(
popUp.unlinkUsages.data.selectedUsagesList as MachineAuthTemplateUsage[]
)
}
buttonText={TEMPLATE_UI_LABELS.UNLINK}
>
<div className="mt-4 text-sm text-mineshaft-400">
This template will no longer be used by the following{" "}
{popUp.unlinkUsages.data?.selectedUsagesList?.length > 1 ? "identities" : "identity"}:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
{(popUp.unlinkUsages.data?.selectedUsagesList as MachineAuthTemplateUsage[])?.map(
(usage) => (
<li key={usage.identityId}>
<div className="mb-1 flex items-center">
<span className="break-all">{usage.identityName}</span>
<Badge
variant="danger"
className="ml-2 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
> >
{usage.identityId} <Td>{usage.identityName}</Td>
</Badge> <Td>
</div> <span className="text-sm text-mineshaft-400">{usage.identityId}</span>
</li> </Td>
) </Tr>
))}
</TBody>
</Table>
{!isPending && usages.length === 0 && (
<EmptyState
title="This template is not currently being used by any identities"
icon={faCertificate}
/>
)} )}
</ul> </TableContainer>
</div> </div>
</DeleteActionModal> </ModalContent>
</> </Modal>
); );
}; };
@@ -89,7 +89,8 @@ const machineIdentityAuthTemplatePermissionSchema = z
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(), [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(), [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(), [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional() [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: z.boolean().optional()
}) })
.optional(); .optional();
@@ -43,6 +43,10 @@ const PERMISSION_ACTIONS = [
{ {
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
label: "Unlink Templates" label: "Unlink Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
label: "Attach Templates"
} }
] as const; ] as const;
@@ -103,7 +107,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true, [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true, [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true, [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
}, },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -116,7 +121,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false, [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false, [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false, [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
}, },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -131,7 +137,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false, [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false, [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false, [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: false
}, },
{ shouldDirty: true } { shouldDirty: true }
); );