mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
General improvements on auth templates
This commit is contained in:
@@ -1352,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url: string;
|
||||
templateId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1365,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url?: string;
|
||||
templateId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -1469,7 +1469,8 @@ export const registerRoutes = async (
|
||||
identityLdapAuthDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
licenseService
|
||||
licenseService,
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
|
||||
@@ -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<string, unknown>;
|
||||
} & Omit<TOrgPermission, "orgId">) => {
|
||||
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<string, unknown> = {};
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export type TLdapTemplateFields = {
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
ldapCaCertificate?: string;
|
||||
};
|
||||
|
||||
// Union type for all template field types
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||

|
||||
|
||||
- See all identities currently using the template
|
||||
- Unlink identities from the template
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
@@ -85,11 +85,11 @@ You can view which identities are using a specific template by clicking **View U
|
||||
</Accordion>
|
||||
|
||||
<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 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 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.
|
||||
|
||||
## 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
|
||||
<Steps>
|
||||
<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 |
|
||||
| `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 |
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<MachineAuthTemplateUsage[]>(
|
||||
`/api/v1/identities/templates/${dto.templateId}/usage`,
|
||||
`/api/v1/identity-templates/${dto.templateId}/delete-usage`,
|
||||
{ identityIds: dto.identityIds },
|
||||
{ params: { organizationId: dto.organizationId } }
|
||||
);
|
||||
|
||||
@@ -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<IdentityAuthTemplate>(
|
||||
`/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<IdentityAuthTemplate[]>(
|
||||
"/api/v1/identities/templates",
|
||||
{
|
||||
params: { authMethod }
|
||||
}
|
||||
);
|
||||
const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/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<MachineAuthTemplateUsage[]>(
|
||||
`/api/v1/identities/templates/${dto.templateId}/usage`,
|
||||
`/api/v1/identity-templates/${dto.templateId}/usage`,
|
||||
{
|
||||
params: { organizationId: dto.organizationId }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface LdapTemplateFields {
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
ldapCaCertificate?: string;
|
||||
}
|
||||
|
||||
export interface IdentityAuthTemplate {
|
||||
|
||||
@@ -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<typeof schema>;
|
||||
@@ -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) =
|
||||
</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>
|
||||
<div className="flex items-center">
|
||||
<span className="capitalize">{template.authMethod}</span>
|
||||
<span className="uppercase">{template.authMethod}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} 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 {
|
||||
MachineIdentityAuthMethod,
|
||||
useAddIdentityLdapAuth,
|
||||
@@ -143,6 +147,12 @@ export const IdentityLdapAuthForm = ({
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
|
||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
|
||||
const { permission } = useOrgPermission();
|
||||
|
||||
const canUseTemplates = permission.can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
@@ -153,11 +163,12 @@ export const IdentityLdapAuthForm = ({
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
scope: "template",
|
||||
scope: "custom",
|
||||
templateId: "",
|
||||
url: "",
|
||||
bindDN: "",
|
||||
@@ -188,15 +199,11 @@ export const IdentityLdapAuthForm = ({
|
||||
// Helper function to determine scope based on existing data
|
||||
const determineScope = (authData: any) => {
|
||||
// If templateId exists in the data, it's template scope
|
||||
// If url, bindDN, bindPass, searchBase exist, it's custom scope
|
||||
if (authData.templateId) {
|
||||
return "template";
|
||||
}
|
||||
if (authData.url || authData.bindDN || authData.bindPass || authData.searchBase) {
|
||||
return "custom";
|
||||
}
|
||||
// Default to template if we can't determine
|
||||
return "template";
|
||||
// Default to custom if we can't determine
|
||||
return "custom";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -228,7 +235,7 @@ export const IdentityLdapAuthForm = ({
|
||||
}
|
||||
|
||||
reset({
|
||||
scope: "template",
|
||||
scope: "custom",
|
||||
templateId: "",
|
||||
url: "",
|
||||
bindDN: "",
|
||||
@@ -345,30 +352,38 @@ export const IdentityLdapAuthForm = ({
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="scope"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Configuration Type"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
{canUseTemplates && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="scope"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Configuration Type"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SelectItem value="template">Use Template</SelectItem>
|
||||
<SelectItem value="custom">Custom Configuration</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
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" && (
|
||||
<Controller
|
||||
@@ -385,6 +400,13 @@ export const IdentityLdapAuthForm = ({
|
||||
value={value}
|
||||
onValueChange={(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"
|
||||
position="popper"
|
||||
@@ -404,66 +426,104 @@ export const IdentityLdapAuthForm = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{scope === "custom" && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="LDAP URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindDN"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Bind Pass"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="********" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="searchBase"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Search Base / DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="LDAP URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ldaps://domain-or-ip:636"
|
||||
type="text"
|
||||
isDisabled={scope === "template"}
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindDN"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
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
|
||||
control={control}
|
||||
@@ -641,9 +701,18 @@ export const IdentityLdapAuthForm = ({
|
||||
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."
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -103,93 +103,93 @@ export const IdentitySection = withPermission(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="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">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
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}
|
||||
<div>
|
||||
<div className="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">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Create Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||
|
||||
{/* Identity Auth Templates Section */}
|
||||
{subscription.machineIdentityAuthTemplates && (
|
||||
<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 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>
|
||||
<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>
|
||||
)}
|
||||
<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} />
|
||||
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<MachineAuthTemplateUsagesModal
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faCertificate, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -22,14 +14,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
MachineAuthTemplateUsage,
|
||||
TEMPLATE_ERROR_MESSAGES,
|
||||
TEMPLATE_UI_LABELS,
|
||||
useGetTemplateUsages,
|
||||
useUnlinkTemplateUsage
|
||||
} from "@app/hooks/api/identityAuthTemplates";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -45,201 +30,51 @@ export const MachineAuthTemplateUsagesModal = ({
|
||||
templateName
|
||||
}: Props) => {
|
||||
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 {
|
||||
data: usages = [],
|
||||
isPending,
|
||||
refetch
|
||||
} = useGetTemplateUsages({
|
||||
const { data: usages = [], isPending } = useGetTemplateUsages({
|
||||
templateId,
|
||||
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 (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent
|
||||
title={`Auth Template Usages: ${templateName}`}
|
||||
subTitle="Manage identities using this template"
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"h-0 flex-shrink-0 overflow-hidden transition-all",
|
||||
selectedUsageIds.length > 0 && "h-16"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||
<div className="mr-2 text-sm">{selectedUsageIds.length} Selected</div>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
<Modal isOpen={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent title={`Usages for Identity Auth Template: ${templateName}`}>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="h-14">
|
||||
<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}`}
|
||||
>
|
||||
{usage.identityId}
|
||||
</Badge>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</DeleteActionModal>
|
||||
</>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -89,7 +89,8 @@ const machineIdentityAuthTemplatePermissionSchema = z
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional()
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ const PERMISSION_ACTIONS = [
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
label: "Unlink Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||
label: "Attach Templates"
|
||||
}
|
||||
] as const;
|
||||
|
||||
@@ -103,7 +107,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
@@ -116,7 +121,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
@@ -131,7 +137,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user