improvement: address requested feedback

This commit is contained in:
Scott Wilson
2024-11-07 12:38:33 -08:00
parent 61f786e8d8
commit 19c26c680c
24 changed files with 263 additions and 193 deletions
@@ -13,7 +13,6 @@ export async function up(knex: Knex): Promise<void> {
t.jsonb("environments").notNullable(); t.jsonb("environments").notNullable();
t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true); t.timestamps(true, true, true);
t.unique(["orgId", "name"]);
}); });
await createOnUpdateTrigger(knex, TableName.ProjectTemplates); await createOnUpdateTrigger(knex, TableName.ProjectTemplates);
+1 -1
View File
@@ -13,7 +13,7 @@ export const ProjectTemplatesSchema = z.object({
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
roles: z.unknown(), roles: z.unknown(),
environments: z.unknown(), environments: z.unknown(),
orgId: z.string().uuid().nullable().optional(), orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()
}); });
@@ -4,16 +4,16 @@ import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
DefaultProjectTemplateIdentifier, import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
ProjectTemplateDefaultEnvironments
} from "@app/ee/services/project-template/project-template-constants";
import { ProjectTemplates } from "@app/lib/api-docs"; import { ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 1_048_576; // 1MB
const SlugSchema = z const SlugSchema = z
.string() .string()
.trim() .trim()
@@ -57,6 +57,9 @@ const ProjectTemplateRolesSchema = z
.superRefine((roles, ctx) => { .superRefine((roles, ctx) => {
if (!roles.length) return; if (!roles.length) return;
if (Buffer.byteLength(JSON.stringify(roles)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
if (new Set(roles.map((v) => v.slug)).size !== roles.length) if (new Set(roles.map((v) => v.slug)).size !== roles.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
@@ -81,6 +84,9 @@ const ProjectTemplateEnvironmentsSchema = z
.array() .array()
.min(1) .min(1)
.superRefine((environments, ctx) => { .superRefine((environments, ctx) => {
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
if (new Set(environments.map((v) => v.name)).size !== environments.length) if (new Set(environments.map((v) => v.name)).size !== environments.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
@@ -116,7 +122,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
handler: async (req) => { handler: async (req) => {
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission); const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
const auditTemplates = projectTemplates.filter((template) => template.name !== DefaultProjectTemplateIdentifier); const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -153,7 +159,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.findProjectTemplatesById( const projectTemplate = await server.services.projectTemplate.findProjectTemplateById(
req.params.templateId, req.params.templateId,
req.permission req.permission
); );
@@ -182,10 +188,10 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
schema: { schema: {
description: "Create a project template.", description: "Create a project template.",
body: z.object({ body: z.object({
name: SlugSchema.refine((val) => val !== DefaultProjectTemplateIdentifier, { name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The project template name "${DefaultProjectTemplateIdentifier}" is reserved.` message: `The requested project template name is reserved.`
}).describe(ProjectTemplates.CREATE.name), }).describe(ProjectTemplates.CREATE.name),
description: z.string().trim().optional().describe(ProjectTemplates.CREATE.description), description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe( environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
ProjectTemplates.CREATE.environments ProjectTemplates.CREATE.environments
@@ -224,12 +230,12 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
description: "Update a project template.", description: "Update a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }), params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
body: z.object({ body: z.object({
name: SlugSchema.refine((val) => val !== DefaultProjectTemplateIdentifier, { name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The project template name "${DefaultProjectTemplateIdentifier}" is reserved.` message: `The requested project template name is reserved.`
}) })
.optional() .optional()
.describe(ProjectTemplates.UPDATE.name), .describe(ProjectTemplates.UPDATE.name),
description: z.string().trim().optional().describe(ProjectTemplates.UPDATE.description), description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles), roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments) environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
}), }),
@@ -1663,7 +1663,7 @@ interface DeleteProjectTemplateEvent {
interface ApplyProjectTemplateEvent { interface ApplyProjectTemplateEvent {
type: EventType.APPLY_PROJECT_TEMPLATE; type: EventType.APPLY_PROJECT_TEMPLATE;
metadata: { metadata: {
templateId: string; template: string;
projectId: string; projectId: string;
}; };
} }
@@ -48,7 +48,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
}, },
pkiEst: false, pkiEst: false,
enforceMfa: false, enforceMfa: false,
projectTemplates: false projectTemplates: true
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -1,25 +1,5 @@
import { TUnpackedPermission } from "@app/ee/services/project-template/project-template-types";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
export const ProjectTemplateDefaultEnvironments = [ export const ProjectTemplateDefaultEnvironments = [
{ name: "Development", slug: "dev", position: 1 }, { name: "Development", slug: "dev", position: 1 },
{ name: "Staging", slug: "staging", position: 2 }, { name: "Staging", slug: "staging", position: 2 },
{ name: "Production", slug: "prod", position: 3 } { name: "Production", slug: "prod", position: 3 }
]; ];
export const DefaultProjectTemplateIdentifier = "default";
export const getDefaultProjectTemplate = (orgId: string) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
name: DefaultProjectTemplateIdentifier,
createdAt: new Date(),
updatedAt: new Date(),
description: "Infisical's default project template",
environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
orgId
});
@@ -0,0 +1,24 @@
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import {
InfisicalProjectTemplate,
TUnpackedPermission
} from "@app/ee/services/project-template/project-template-types";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
export const getDefaultProjectTemplate = (orgId: string) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
name: InfisicalProjectTemplate.Default,
createdAt: new Date(),
updatedAt: new Date(),
description: "Infisical's default project template",
environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
orgId
});
export const isInfisicalProjectTemplate = (template: string) =>
Object.values(InfisicalProjectTemplate).includes(template as InfisicalProjectTemplate);
@@ -5,7 +5,7 @@ import { TProjectTemplates } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-constants"; import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { import {
TCreateProjectTemplateDTO, TCreateProjectTemplateDTO,
TProjectTemplateEnvironment, TProjectTemplateEnvironment,
@@ -50,6 +50,13 @@ export const projectTemplateServiceFactory = ({
projectTemplateDAL projectTemplateDAL
}: TProjectTemplatesServiceFactoryDep) => { }: TProjectTemplatesServiceFactoryDep) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => { const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
});
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -60,13 +67,6 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplates = await projectTemplateDAL.find({ const projectTemplates = await projectTemplateDAL.find({
orgId: actor.orgId orgId: actor.orgId
}); });
@@ -77,17 +77,7 @@ export const projectTemplateServiceFactory = ({
]; ];
}; };
const findProjectTemplatesById = async (id: string, actor: OrgServiceActor) => { const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)
@@ -95,13 +85,48 @@ export const projectTemplateServiceFactory = ({
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates." message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
}); });
const [projectTemplate] = await projectTemplateDAL.find({ const projectTemplate = await projectTemplateDAL.findOne({ name, orgId: actor.orgId });
id,
orgId: actor.orgId // ensure from user org if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
});
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
return {
...$unpackProjectTemplate(projectTemplate),
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
};
};
const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
return { return {
...$unpackProjectTemplate(projectTemplate), ...$unpackProjectTemplate(projectTemplate),
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
@@ -112,6 +137,13 @@ export const projectTemplateServiceFactory = ({
{ roles, environments, ...params }: TCreateProjectTemplateDTO, { roles, environments, ...params }: TCreateProjectTemplateDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
});
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -122,11 +154,16 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
const plan = await licenseService.getPlan(actor.orgId); const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
orgId: actor.orgId
})
);
if (!plan.projectTemplates) if (isConflictingName)
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates." message: `A project template with the name "${params.name}" already exists.`
}); });
const projectTemplate = await projectTemplateDAL.create({ const projectTemplate = await projectTemplateDAL.create({
@@ -144,16 +181,6 @@ export const projectTemplateServiceFactory = ({
{ roles, environments, ...params }: TUpdateProjectTemplateDTO, { roles, environments, ...params }: TUpdateProjectTemplateDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)
@@ -161,7 +188,35 @@ export const projectTemplateServiceFactory = ({
message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates." message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates."
}); });
const projectTemplate = await projectTemplateDAL.updateById(id, { const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (params.name && projectTemplate.name !== params.name) {
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
orgId: projectTemplate.orgId
})
);
if (isConflictingName)
throw new BadRequestError({
message: `A project template with the name "${params.name}" already exists.`
});
}
const updatedProjectTemplate = await projectTemplateDAL.updateById(id, {
...params, ...params,
roles: roles roles: roles
? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))) ? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) })))
@@ -169,20 +224,10 @@ export const projectTemplateServiceFactory = ({
environments: environments ? JSON.stringify(environments) : undefined environments: environments ? JSON.stringify(environments) : undefined
}); });
return $unpackProjectTemplate(projectTemplate); return $unpackProjectTemplate(updatedProjectTemplate);
}; };
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => { const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)
@@ -190,9 +235,23 @@ export const projectTemplateServiceFactory = ({
message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates." message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates."
}); });
const projectTemplate = await projectTemplateDAL.deleteById(id); const projectTemplate = await projectTemplateDAL.findById(id);
return $unpackProjectTemplate(projectTemplate); if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
const deletedProjectTemplate = await projectTemplateDAL.deleteById(id);
return $unpackProjectTemplate(deletedProjectTemplate);
}; };
return { return {
@@ -200,6 +259,7 @@ export const projectTemplateServiceFactory = ({
createProjectTemplate, createProjectTemplate,
updateProjectTemplateById, updateProjectTemplateById,
deleteProjectTemplateById, deleteProjectTemplateById,
findProjectTemplatesById findProjectTemplateById,
findProjectTemplateByName
}; };
}; };
@@ -22,3 +22,7 @@ export type TCreateProjectTemplateDTO = {
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>; export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>; export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
export enum InfisicalProjectTemplate {
Default = "default"
}
+1 -1
View File
@@ -392,7 +392,7 @@ export const PROJECTS = {
organizationSlug: "The slug of the organization to create the project in.", organizationSlug: "The slug of the organization to create the project in.",
projectName: "The name of the project to create.", projectName: "The name of the project to create.",
slug: "An optional slug for the project.", slug: "An optional slug for the project.",
templateId: "The ID of the project template, if specified, to apply to this project." template: "The name of the project template, if specified, to apply to this project."
}, },
DELETE: { DELETE: {
workspaceId: "The ID of the project to delete." workspaceId: "The ID of the project to delete."
+12 -4
View File
@@ -9,6 +9,7 @@ import {
ProjectKeysSchema ProjectKeysSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@@ -170,7 +171,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.optional() .optional()
.describe(PROJECTS.CREATE.slug), .describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional(), kmsKeyId: z.string().optional(),
templateId: z.string().uuid().optional().describe(PROJECTS.CREATE.templateId) template: z
.string()
.refine((v) => slugify(v) === v, {
message: "Template name must be in slug format"
})
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -188,7 +196,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceName: req.body.projectName, workspaceName: req.body.projectName,
slug: req.body.slug, slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId, kmsKeyId: req.body.kmsKeyId,
templateId: req.body.templateId template: req.body.template
}); });
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
@@ -201,14 +209,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
if (req.body.templateId) { if (req.body.template) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
event: { event: {
type: EventType.APPLY_PROJECT_TEMPLATE, type: EventType.APPLY_PROJECT_TEMPLATE,
metadata: { metadata: {
templateId: req.body.templateId, template: req.body.template,
projectId: project.id projectId: project.id
} }
} }
+13 -16
View File
@@ -7,6 +7,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
@@ -152,7 +153,7 @@ export const projectServiceFactory = ({
kmsKeyId, kmsKeyId,
tx: trx, tx: trx,
createDefaultEnvs = true, createDefaultEnvs = true,
templateId template = InfisicalProjectTemplate.Default
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -187,23 +188,19 @@ export const projectServiceFactory = ({
} }
} }
let projectTemplate: Awaited<ReturnType<typeof projectTemplateService.findProjectTemplatesById>> | null = null; let projectTemplate: Awaited<ReturnType<typeof projectTemplateService.findProjectTemplateByName>> | null = null;
if (templateId) { switch (template) {
if (!plan.projectTemplates) case InfisicalProjectTemplate.Default:
throw new BadRequestError({ projectTemplate = null;
message: break;
"Failed to apply project template due to plan restriction. Upgrade plan to access project templates." default:
projectTemplate = await projectTemplateService.findProjectTemplateByName(template, {
id: actorId,
orgId: organization.id,
type: actor,
authMethod: actorAuthMethod
}); });
projectTemplate = await projectTemplateService.findProjectTemplatesById(templateId, {
id: actorId,
orgId: organization.id,
type: actor,
authMethod: actorAuthMethod
});
if (!projectTemplate) throw new NotFoundError({ message: `Project template with ID ${templateId} not found.` });
} }
const project = await projectDAL.create( const project = await projectDAL.create(
@@ -32,7 +32,7 @@ export type TCreateProjectDTO = {
slug?: string; slug?: string;
kmsKeyId?: string; kmsKeyId?: string;
createDefaultEnvs?: boolean; createDefaultEnvs?: boolean;
templateId?: string; template?: string;
tx?: Knex; tx?: Knex;
}; };
@@ -105,7 +105,7 @@ In the following steps, we'll explore how to use a project template when creatin
Your project will be provisioned with the configured template roles and environments. Your project will be provisioned with the configured template roles and environments.
</Tab> </Tab>
<Tab title="API"> <Tab title="API">
To use a project template, make an API request to the [Create Project](/api-reference/endpoints/workspaces/create-workspace) API endpoint with the specified template ID included. To use a project template, make an API request to the [Create Project](/api-reference/endpoints/workspaces/create-workspace) API endpoint with the specified template name included.
### Sample request ### Sample request
@@ -115,7 +115,7 @@ In the following steps, we'll explore how to use a project template when creatin
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data '{ --data '{
"projectName": "My Project", "projectName": "My Project",
"templateId": "<template-id>", "template": "<template-name>", // defaults to "default"
}' }'
``` ```
@@ -26,4 +26,6 @@ export type TDeleteProjectTemplateDTO = {
templateId: string; templateId: string;
}; };
export const DefaultProjectTemplateIdentifier = "default"; export enum InfisicalProjectTemplate {
Default = "default"
}
+4 -4
View File
@@ -209,20 +209,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
export const createWorkspace = ({ export const createWorkspace = ({
projectName, projectName,
kmsKeyId, kmsKeyId,
templateId template
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => { }: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, templateId }); return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, template });
}; };
export const useCreateWorkspace = () => { export const useCreateWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, kmsKeyId, templateId }) => mutationFn: async ({ projectName, kmsKeyId, template }) =>
createWorkspace({ createWorkspace({
projectName, projectName,
kmsKeyId, kmsKeyId,
templateId template
}), }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+1 -1
View File
@@ -57,7 +57,7 @@ export type TGetUpgradeProjectStatusDTO = {
export type CreateWorkspaceDTO = { export type CreateWorkspaceDTO = {
projectName: string; projectName: string;
kmsKeyId?: string; kmsKeyId?: string;
templateId?: string; template?: string;
}; };
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
+18 -27
View File
@@ -79,10 +79,7 @@ import {
useSelectOrganization useSelectOrganization
} from "@app/hooks/api"; } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
DefaultProjectTemplateIdentifier,
useListProjectTemplates
} from "@app/hooks/api/projectTemplates";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
@@ -130,7 +127,7 @@ const formSchema = yup.object({
.max(64, "Too long, maximum length is 64 characters"), .max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members"), addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID"), kmsKeyId: yup.string().label("KMS Key ID"),
templateId: yup.string().label("Project Template ID") template: yup.string().label("Project Template Name")
}); });
type TAddProjectFormData = yup.InferType<typeof formSchema>; type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -288,12 +285,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
enabled: canReadProjectTemplates && subscription?.projectTemplates enabled: canReadProjectTemplates && subscription?.projectTemplates
}); });
const onCreateProject = async ({ const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
name,
addMembers,
kmsKeyId,
templateId
}: TAddProjectFormData) => {
// type check // type check
if (!currentOrg) return; if (!currentOrg) return;
if (!user) return; if (!user) return;
@@ -305,7 +297,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
} = await createWs.mutateAsync({ } = await createWs.mutateAsync({
projectName: name, projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
templateId: templateId === DefaultProjectTemplateIdentifier ? undefined : templateId template
}); });
if (addMembers) { if (addMembers) {
@@ -948,7 +940,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
/> />
<Controller <Controller
control={control} control={control}
name="templateId" name="template"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Read} I={OrgPermissionActions.Read}
@@ -971,25 +963,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
} }
> >
<Select <Select
defaultValue={DefaultProjectTemplateIdentifier} defaultValue={InfisicalProjectTemplate.Default}
placeholder={DefaultProjectTemplateIdentifier} placeholder={InfisicalProjectTemplate.Default}
isDisabled={!isAllowed || !subscription?.projectTemplates} isDisabled={!isAllowed || !subscription?.projectTemplates}
value={value} value={value}
onValueChange={onChange} onValueChange={onChange}
className="w-44" className="w-44"
> >
<SelectItem value={DefaultProjectTemplateIdentifier}> {projectTemplates.length
{DefaultProjectTemplateIdentifier} ? projectTemplates.map((template) => (
</SelectItem> <SelectItem key={template.id} value={template.name}>
{projectTemplates {template.name}
.filter( </SelectItem>
(template) => template.name !== DefaultProjectTemplateIdentifier ))
) : Object.values(InfisicalProjectTemplate).map((template) => (
.map((template) => ( <SelectItem key={template} value={template}>
<SelectItem key={template.id} value={template.id}> {template}
{template.name} </SelectItem>
</SelectItem> ))}
))}
</Select> </Select>
</FormControl> </FormControl>
)} )}
+18 -27
View File
@@ -70,10 +70,7 @@ import {
useRegisterUserAction useRegisterUserAction
} from "@app/hooks/api"; } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
DefaultProjectTemplateIdentifier,
useListProjectTemplates
} from "@app/hooks/api/projectTemplates";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
@@ -488,7 +485,7 @@ const formSchema = yup.object({
.max(64, "Too long, maximum length is 64 characters"), .max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members"), addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID"), kmsKeyId: yup.string().label("KMS Key ID"),
templateId: yup.string().label("Project Template ID") template: yup.string().label("Project Template Name")
}); });
type TAddProjectFormData = yup.InferType<typeof formSchema>; type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -543,12 +540,7 @@ const OrganizationPage = () => {
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms) enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
}); });
const onCreateProject = async ({ const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
name,
addMembers,
kmsKeyId,
templateId
}: TAddProjectFormData) => {
// type check // type check
if (!currentOrg) return; if (!currentOrg) return;
if (!user) return; if (!user) return;
@@ -560,7 +552,7 @@ const OrganizationPage = () => {
} = await createWs.mutateAsync({ } = await createWs.mutateAsync({
projectName: name, projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
templateId: templateId === DefaultProjectTemplateIdentifier ? undefined : templateId template
}); });
if (addMembers) { if (addMembers) {
@@ -1076,7 +1068,7 @@ const OrganizationPage = () => {
/> />
<Controller <Controller
control={control} control={control}
name="templateId" name="template"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Read} I={OrgPermissionActions.Read}
@@ -1099,25 +1091,24 @@ const OrganizationPage = () => {
} }
> >
<Select <Select
defaultValue={DefaultProjectTemplateIdentifier} defaultValue={InfisicalProjectTemplate.Default}
placeholder={DefaultProjectTemplateIdentifier} placeholder={InfisicalProjectTemplate.Default}
isDisabled={!isAllowed || !subscription?.projectTemplates} isDisabled={!isAllowed || !subscription?.projectTemplates}
value={value} value={value}
onValueChange={onChange} onValueChange={onChange}
className="w-44" className="w-44"
> >
<SelectItem value={DefaultProjectTemplateIdentifier}> {projectTemplates.length
{DefaultProjectTemplateIdentifier} ? projectTemplates.map((template) => (
</SelectItem> <SelectItem key={template.id} value={template.name}>
{projectTemplates {template.name}
.filter( </SelectItem>
(template) => template.name !== DefaultProjectTemplateIdentifier ))
) : Object.values(InfisicalProjectTemplate).map((template) => (
.map((template) => ( <SelectItem key={template} value={template}>
<SelectItem key={template.id} value={template.id}> {template}
{template.name} </SelectItem>
</SelectItem> ))}
))}
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, EmptyState, Spinner } from "@app/components/v2"; import { Button, EmptyState, Spinner } from "@app/components/v2";
import { import {
DefaultProjectTemplateIdentifier, InfisicalProjectTemplate,
TProjectTemplate, TProjectTemplate,
useGetProjectTemplateById useGetProjectTemplateById
} from "@app/hooks/api/projectTemplates"; } from "@app/hooks/api/projectTemplates";
@@ -16,11 +16,13 @@ type Props = {
}; };
export const EditProjectTemplateSection = ({ template, onBack }: Props) => { export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
const isDefault = template.name === DefaultProjectTemplateIdentifier; const isInfisicalTemplate = Object.values(InfisicalProjectTemplate).includes(
template.name as InfisicalProjectTemplate
);
const { data: projectTemplate, isLoading } = useGetProjectTemplateById(template.id, { const { data: projectTemplate, isLoading } = useGetProjectTemplateById(template.id, {
initialData: template, initialData: template,
enabled: !isDefault enabled: !isInfisicalTemplate
}); });
return ( return (
@@ -41,7 +43,7 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
</div> </div>
) : projectTemplate ? ( ) : projectTemplate ? (
<EditProjectTemplate <EditProjectTemplate
isDefault={isDefault} isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate} projectTemplate={projectTemplate}
onBack={onBack} onBack={onBack}
/> />
@@ -23,10 +23,10 @@ import { ProjectTemplateRolesSection } from "./ProjectTemplateRolesSection";
type Props = { type Props = {
projectTemplate: TProjectTemplate; projectTemplate: TProjectTemplate;
onBack: () => void; onBack: () => void;
isDefault: boolean; isInfisicalTemplate: boolean;
}; };
export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Props) => { export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBack }: Props) => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeTemplate", "removeTemplate",
"editDetails" "editDetails"
@@ -63,7 +63,7 @@ export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Prop
<h3 className="text-xl font-semibold">{name}</h3> <h3 className="text-xl font-semibold">{name}</h3>
<h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2> <h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2>
</div> </div>
{!isDefault && ( {!isInfisicalTemplate && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<IconButton <IconButton
@@ -117,9 +117,12 @@ export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Prop
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
<ProjectTemplateEnvironmentsForm isDefault={isDefault} projectTemplate={projectTemplate} /> <ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
<ProjectTemplateRolesSection <ProjectTemplateRolesSection
isDefaultTemplate={isDefault} isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate} projectTemplate={projectTemplate}
/> />
<ProjectTemplateDetailsModal <ProjectTemplateDetailsModal
@@ -169,7 +169,7 @@ export const ProjectTemplateEditRoleForm = ({
label="Name" label="Name"
className="mb-0 flex-1" className="mb-0 flex-1"
> >
<Input {...field} placeholder="Role name..." /> <Input {...field} autoFocus placeholder="Role name..." />
</FormControl> </FormControl>
)} )}
/> />
@@ -25,7 +25,7 @@ import { slugSchema } from "@app/lib/schemas";
type Props = { type Props = {
projectTemplate: TProjectTemplate; projectTemplate: TProjectTemplate;
isDefault: boolean; isInfisicalTemplate: boolean;
}; };
const formSchema = z.object({ const formSchema = z.object({
@@ -39,7 +39,10 @@ const formSchema = z.object({
type TFormSchema = z.infer<typeof formSchema>; type TFormSchema = z.infer<typeof formSchema>;
export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }: Props) => { export const ProjectTemplateEnvironmentsForm = ({
projectTemplate,
isInfisicalTemplate
}: Props) => {
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -94,13 +97,13 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<div> <div>
<h2 className="text-lg font-semibold">Project Environments</h2> <h2 className="text-lg font-semibold">Project Environments</h2>
{!isDefault && ( {!isInfisicalTemplate && (
<p className="text-sm text-mineshaft-400"> <p className="text-sm text-mineshaft-400">
Add, rename, remove and reorder environments for this project template Add, rename, remove and reorder environments for this project template
</p> </p>
)} )}
</div> </div>
{!isDefault && ( {!isInfisicalTemplate && (
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates} a={OrgPermissionSubjects.ProjectTemplates}
@@ -129,7 +132,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
<Tr> <Tr>
<Th>Friendly Name</Th> <Th>Friendly Name</Th>
<Th>Slug</Th> <Th>Slug</Th>
{!isDefault && ( {!isInfisicalTemplate && (
<Th> <Th>
<div className="flex w-full justify-end normal-case"> <div className="flex w-full justify-end normal-case">
<OrgPermissionCan <OrgPermissionCan
@@ -161,7 +164,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
{environments.map(({ id, name, slug }, pos) => ( {environments.map(({ id, name, slug }, pos) => (
<Tr key={id}> <Tr key={id}>
<Td> <Td>
{isDefault ? ( {isInfisicalTemplate ? (
name name
) : ( ) : (
<OrgPermissionCan <OrgPermissionCan
@@ -187,7 +190,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
)} )}
</Td> </Td>
<Td> <Td>
{isDefault ? ( {isInfisicalTemplate ? (
slug slug
) : ( ) : (
<OrgPermissionCan <OrgPermissionCan
@@ -212,7 +215,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
</OrgPermissionCan> </OrgPermissionCan>
)} )}
</Td> </Td>
{!isDefault && ( {!isInfisicalTemplate && (
<Td className="flex items-center justify-end"> <Td className="flex items-center justify-end">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
@@ -27,10 +27,10 @@ import { ProjectTemplateEditRoleForm } from "./ProjectTemplateEditRoleForm";
type Props = { type Props = {
projectTemplate: TProjectTemplate; projectTemplate: TProjectTemplate;
isDefaultTemplate: boolean; isInfisicalTemplate: boolean;
}; };
export const ProjectTemplateRolesSection = ({ projectTemplate, isDefaultTemplate }: Props) => { export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTemplate }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"removeRole", "removeRole",
"editRole" "editRole"
@@ -108,12 +108,12 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isDefaultTemplate
<div> <div>
<h2 className="text-lg font-semibold">Project Roles</h2> <h2 className="text-lg font-semibold">Project Roles</h2>
<p className="text-sm text-mineshaft-400"> <p className="text-sm text-mineshaft-400">
{isDefaultTemplate {isInfisicalTemplate
? "Click a role to view the associated permissions" ? "Click a role to view the associated permissions"
: "Add, edit and remove roles for this project template"} : "Add, edit and remove roles for this project template"}
</p> </p>
</div> </div>
{!isDefaultTemplate && ( {!isInfisicalTemplate && (
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates} a={OrgPermissionSubjects.ProjectTemplates}