diff --git a/backend/src/ee/services/project-template/project-template-fns.ts b/backend/src/ee/services/project-template/project-template-fns.ts index 269da7766..8e8ebfa13 100644 --- a/backend/src/ee/services/project-template/project-template-fns.ts +++ b/backend/src/ee/services/project-template/project-template-fns.ts @@ -13,7 +13,7 @@ export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ( name: InfisicalProjectTemplate.Default, createdAt: new Date(), updatedAt: new Date(), - description: `Infisical's ${type ? `"${type}"` : ""} default project template`, + description: `Infisical's ${type} default project template`, environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null, roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map( ({ name, slug, permissions }) => ({ diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index dcf6cd3ba..5b6163977 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -160,6 +160,17 @@ export const projectTemplateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); + if (environments && type !== ProjectType.SecretManager) { + throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" }); + } + + if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) { + throw new BadRequestError({ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + message: `Failed to create project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.` + }); + } + const isConflictingName = Boolean( await projectTemplateDAL.findOne({ name: params.name, @@ -216,6 +227,13 @@ export const projectTemplateServiceFactory = ({ if (projectTemplate.type === ProjectType.SecretManager && environments === null) throw new BadRequestError({ message: "Environments cannot be removed for SecretManager project templates" }); + if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) { + throw new BadRequestError({ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + message: `Failed to update project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.` + }); + } + if (params.name && projectTemplate.name !== params.name) { const isConflictingName = Boolean( await projectTemplateDAL.findOne({ diff --git a/backend/src/services/project-role/project-role-fns.ts b/backend/src/services/project-role/project-role-fns.ts index 37aa90105..4dfcf960b 100644 --- a/backend/src/services/project-role/project-role-fns.ts +++ b/backend/src/services/project-role/project-role-fns.ts @@ -1,3 +1,5 @@ +import { v4 as uuidv4 } from "uuid"; + import { ProjectMembershipRole, ProjectType } from "@app/db/schemas"; import { cryptographicOperatorPermissions, @@ -12,7 +14,7 @@ import { TGetPredefinedRolesDTO } from "@app/services/project-role/project-role- export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetPredefinedRolesDTO) => { return [ { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid + id: uuidv4(), projectId, name: "Admin", slug: ProjectMembershipRole.Admin, @@ -22,7 +24,7 @@ export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetP updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "Developer", slug: ProjectMembershipRole.Member, @@ -32,7 +34,7 @@ export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetP updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c73", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "SSH Host Bootstrapper", slug: ProjectMembershipRole.SshHostBootstrapper, @@ -43,7 +45,7 @@ export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetP type: ProjectType.SSH }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c74", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "Cryptographic Operator", slug: ProjectMembershipRole.KmsCryptographicOperator, @@ -54,7 +56,7 @@ export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetP type: ProjectType.KMS }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "Viewer", slug: ProjectMembershipRole.Viewer, @@ -64,7 +66,7 @@ export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetP updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "No Access", slug: ProjectMembershipRole.NoAccess, @@ -73,5 +75,5 @@ export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetP createdAt: new Date(), updatedAt: new Date() } - ].filter(({ slug, type }) => (type ? type === projectType : true) && (!roleFilter || roleFilter.includes(slug))); + ].filter(({ slug, type }) => (type ? type === projectType : true) && (!roleFilter || roleFilter === slug)); }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 2e45904fd..babcf7d9c 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -117,11 +117,14 @@ export const projectRoleServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) { - const predefinedRole = getPredefinedRoles({ + const [predefinedRole] = getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType, roleFilter: roleSlug as ProjectMembershipRole - })[0]; + }); + + if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` }); + return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) }; } diff --git a/docs/documentation/platform/project-templates.mdx b/docs/documentation/platform/project-templates.mdx index c98a679a9..7dd5ceb50 100644 --- a/docs/documentation/platform/project-templates.mdx +++ b/docs/documentation/platform/project-templates.mdx @@ -33,7 +33,7 @@ In the following steps, we'll explore how to set up a project template. - Navigate to the Project Templates tab on the Feature Settings page for the project type you want to create a template for and tap on the **Add Template** button. + Navigate to the **Project Templates** tab on the Feature Settings page for the project type you want to create a template for and tap on the **Add Template** button. ![project template add button](/images/platform/project-templates/project-template-add-button.png) Specify your template details. Here's some guidance on each field: diff --git a/docs/documentation/platform/ssh/overview.mdx b/docs/documentation/platform/ssh/overview.mdx index 29bd317ec..a252e1f71 100644 --- a/docs/documentation/platform/ssh/overview.mdx +++ b/docs/documentation/platform/ssh/overview.mdx @@ -50,6 +50,10 @@ we will register a remote host with Infisical through a [machine identity](/docu This role grants the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical and establish the necessary configuration on it. + + If you plan to use a custom role to bootstrap SSH hosts, ensure the role has the **Create** and **Issue Host Certificates** on the **SSH Host** resource. + + ![ssh add identity to project](/images/platform/ssh/v2/ssh-add-identity-to-project.png) diff --git a/docs/images/platform/project-templates/project-template-create.png b/docs/images/platform/project-templates/project-template-create.png index 44e7824ed..6c485b4cc 100644 Binary files a/docs/images/platform/project-templates/project-template-create.png and b/docs/images/platform/project-templates/project-template-create.png differ diff --git a/frontend/public/images/project-templates/project-templates-new-location.png b/frontend/public/images/project-templates/project-templates-new-location.png index c71c5c938..dd08f19a6 100644 Binary files a/frontend/public/images/project-templates/project-templates-new-location.png and b/frontend/public/images/project-templates/project-templates-new-location.png differ diff --git a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx index 4cb039ad3..ef2691d69 100644 --- a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx @@ -19,7 +19,7 @@ import { THead, Tr } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { slugSchema } from "@app/lib/schemas"; @@ -56,6 +56,8 @@ export const ProjectTemplateEnvironmentsForm = ({ resolver: zodResolver(formSchema) }); + const { subscription } = useSubscription(); + const { fields: environments, move, @@ -90,6 +92,9 @@ export const ProjectTemplateEnvironmentsForm = ({ } }; + const isEnvironmentLimitExceeded = + Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit; + return (
{(isAllowed) => ( diff --git a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx index ed73dc005..5b5728dac 100644 --- a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx @@ -57,7 +57,13 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { }); const onFormSubmit = async (data: FormData) => { - if (!projectType) return; + if (!projectType) { + createNotification({ + text: "Failed to determine project type", + type: "error" + }); + return; + } const mutation = projectTemplate ? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data }) diff --git a/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx b/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx index 796b6b8e9..c178aca22 100644 --- a/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx +++ b/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx @@ -7,7 +7,7 @@ export const CertManagerSettingsPage = () => { return ( <> - Cert Managment Settings + Cert Management Settings
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx index 9fc87830a..cb3ac2f85 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx @@ -53,7 +53,7 @@ export const NewPermissionRule = ({ onClose }: Props) => { (