diff --git a/backend/src/db/migrations/20250505203703_project-templates-type-col.ts b/backend/src/db/migrations/20250505203703_project-templates-type-col.ts new file mode 100644 index 000000000..d1ef14d72 --- /dev/null +++ b/backend/src/db/migrations/20250505203703_project-templates-type-col.ts @@ -0,0 +1,22 @@ +import { Knex } from "knex"; + +import { ProjectType, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.ProjectTemplates, "type"))) { + await knex.schema.alterTable(TableName.ProjectTemplates, (t) => { + // defaulting to sm for migration to set existing, new ones will always be specified on creation + t.string("type").defaultTo(ProjectType.SecretManager).notNullable(); + t.jsonb("environments").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.ProjectTemplates, "type")) { + await knex.schema.alterTable(TableName.ProjectTemplates, (t) => { + t.dropColumn("type"); + // not reverting nullable environments + }); + } +} diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 7fd77da6c..60a13bac6 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -185,11 +185,16 @@ export enum OrgMembershipStatus { } export enum ProjectMembershipRole { + // general Admin = "admin", Member = "member", Custom = "custom", Viewer = "viewer", - NoAccess = "no-access" + NoAccess = "no-access", + // ssh + SshHostBootstrapper = "ssh-host-bootstrapper", + // kms + KmsCryptographicOperator = "cryptographic-operator" } export enum SecretEncryptionAlgo { diff --git a/backend/src/db/schemas/project-templates.ts b/backend/src/db/schemas/project-templates.ts index 68f37d256..f12386165 100644 --- a/backend/src/db/schemas/project-templates.ts +++ b/backend/src/db/schemas/project-templates.ts @@ -12,10 +12,11 @@ export const ProjectTemplatesSchema = z.object({ name: z.string(), description: z.string().nullable().optional(), roles: z.unknown(), - environments: z.unknown(), + environments: z.unknown().nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + type: z.string().default("secret-manager") }); export type TProjectTemplates = z.infer; diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 08d16414b..5d33b4d58 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -1,9 +1,8 @@ import { z } from "zod"; -import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectTemplatesSchema, ProjectType } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -35,6 +34,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({ position: z.number().min(1) }) .array() + .nullable() }); const ProjectTemplateRolesSchema = z @@ -104,6 +104,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) hide: false, tags: [ApiDocsTags.ProjectTemplates], description: "List project templates for the current organization.", + querystring: z.object({ + type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type) + }), response: { 200: z.object({ projectTemplates: SanitizedProjectTemplateSchema.array() @@ -112,7 +115,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission); + const { type } = req.query; + const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission, type); const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name)); @@ -184,6 +188,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) tags: [ApiDocsTags.ProjectTemplates], description: "Create a project template.", body: z.object({ + type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type), name: slugSchema({ field: "name" }) .refine((val) => !isInfisicalProjectTemplate(val), { message: `The requested project template name is reserved.` @@ -191,9 +196,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) .describe(ProjectTemplates.CREATE.name), description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), - environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe( - ProjectTemplates.CREATE.environments - ) + environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional() }), response: { 200: z.object({ diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts new file mode 100644 index 000000000..44fcb7825 --- /dev/null +++ b/backend/src/ee/services/permission/default-roles.ts @@ -0,0 +1,448 @@ +import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; + +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionGroupActions, + ProjectPermissionIdentityActions, + ProjectPermissionKmipActions, + ProjectPermissionMemberActions, + ProjectPermissionSecretActions, + ProjectPermissionSecretRotationActions, + ProjectPermissionSecretSyncActions, + ProjectPermissionSet, + ProjectPermissionSshHostActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; + +const buildAdminPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + // Admins get full access to everything + [ + ProjectPermissionSub.SecretFolders, + ProjectPermissionSub.SecretImports, + ProjectPermissionSub.SecretApproval, + ProjectPermissionSub.Role, + ProjectPermissionSub.Integrations, + ProjectPermissionSub.Webhooks, + ProjectPermissionSub.ServiceTokens, + ProjectPermissionSub.Settings, + ProjectPermissionSub.Environments, + ProjectPermissionSub.Tags, + ProjectPermissionSub.AuditLogs, + ProjectPermissionSub.IpAllowList, + ProjectPermissionSub.CertificateAuthorities, + ProjectPermissionSub.CertificateTemplates, + ProjectPermissionSub.PkiAlerts, + ProjectPermissionSub.PkiCollections, + ProjectPermissionSub.SshCertificateAuthorities, + ProjectPermissionSub.SshCertificates, + ProjectPermissionSub.SshCertificateTemplates, + ProjectPermissionSub.SshHostGroups + ].forEach((el) => { + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + el + ); + }); + + can( + [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.Edit, + ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Delete, + ProjectPermissionCertificateActions.ReadPrivateKey + ], + ProjectPermissionSub.Certificates + ); + + can( + [ + ProjectPermissionSshHostActions.Edit, + ProjectPermissionSshHostActions.Read, + ProjectPermissionSshHostActions.Create, + ProjectPermissionSshHostActions.Delete, + ProjectPermissionSshHostActions.IssueHostCert + ], + ProjectPermissionSub.SshHosts + ); + + can( + [ + ProjectPermissionMemberActions.Create, + ProjectPermissionMemberActions.Edit, + ProjectPermissionMemberActions.Delete, + ProjectPermissionMemberActions.Read, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionMemberActions.AssumePrivileges + ], + ProjectPermissionSub.Member + ); + + can( + [ + ProjectPermissionGroupActions.Create, + ProjectPermissionGroupActions.Edit, + ProjectPermissionGroupActions.Delete, + ProjectPermissionGroupActions.Read, + ProjectPermissionGroupActions.GrantPrivileges + ], + ProjectPermissionSub.Groups + ); + + can( + [ + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Delete, + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionIdentityActions.AssumePrivileges + ], + ProjectPermissionSub.Identity + ); + + can( + [ + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + + can( + [ + ProjectPermissionDynamicSecretActions.ReadRootCredential, + ProjectPermissionDynamicSecretActions.EditRootCredential, + ProjectPermissionDynamicSecretActions.CreateRootCredential, + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + + can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); + can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); + can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); + can( + [ + ProjectPermissionCmekActions.Create, + ProjectPermissionCmekActions.Edit, + ProjectPermissionCmekActions.Delete, + ProjectPermissionCmekActions.Read, + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify + ], + ProjectPermissionSub.Cmek + ); + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + + can( + [ + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionKmipActions.ReadClients, + ProjectPermissionKmipActions.GenerateClientCertificates + ], + ProjectPermissionSub.Kmip + ); + + can( + [ + ProjectPermissionSecretRotationActions.Create, + ProjectPermissionSecretRotationActions.Edit, + ProjectPermissionSecretRotationActions.Delete, + ProjectPermissionSecretRotationActions.Read, + ProjectPermissionSecretRotationActions.ReadGeneratedCredentials, + ProjectPermissionSecretRotationActions.RotateSecrets + ], + ProjectPermissionSub.SecretRotation + ); + + return rules; +}; + +const buildMemberPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can( + [ + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.SecretFolders + ); + can( + [ + ProjectPermissionDynamicSecretActions.ReadRootCredential, + ProjectPermissionDynamicSecretActions.EditRootCredential, + ProjectPermissionDynamicSecretActions.CreateRootCredential, + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.SecretImports + ); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); + can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); + + can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); + + can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member); + + can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Integrations + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Webhooks + ); + + can( + [ + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Delete + ], + ProjectPermissionSub.Identity + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.ServiceTokens + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Settings + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Environments + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Tags + ); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.Role); + can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs); + can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList); + + // double check if all CRUD are needed for CA and Certificates + can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); + + can( + [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.Edit, + ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Delete + ], + ProjectPermissionSub.Certificates + ); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); + can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); + can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); + + can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); + + can( + [ + ProjectPermissionCmekActions.Create, + ProjectPermissionCmekActions.Edit, + ProjectPermissionCmekActions.Delete, + ProjectPermissionCmekActions.Read, + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify + ], + ProjectPermissionSub.Cmek + ); + + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + + return rules; +}; + +const buildViewerPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); + can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); + can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); + can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); + can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); + + return rules; +}; + +const buildNoAccessProjectPermission = () => { + const { rules } = new AbilityBuilder>(createMongoAbility); + return rules; +}; + +const buildSshHostBootstrapPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can( + [ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.IssueHostCert], + ProjectPermissionSub.SshHosts + ); + + return rules; +}; + +const buildCryptographicOperatorPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can( + [ + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify + ], + ProjectPermissionSub.Cmek + ); + + return rules; +}; + +// General +export const projectAdminPermissions = buildAdminPermissionRules(); +export const projectMemberPermissions = buildMemberPermissionRules(); +export const projectViewerPermission = buildViewerPermissionRules(); +export const projectNoAccessPermissions = buildNoAccessProjectPermission(); + +// SSH +export const sshHostBootstrapPermissions = buildSshHostBootstrapPermissionRules(); + +// KMS +export const cryptographicOperatorPermissions = buildCryptographicOperatorPermissionRules(); diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 3d2f96f82..9653af942 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -12,6 +12,14 @@ import { TIdentityProjectMemberships, TProjectMemberships } from "@app/db/schemas"; +import { + cryptographicOperatorPermissions, + projectAdminPermissions, + projectMemberPermissions, + projectNoAccessPermissions, + projectViewerPermission, + sshHostBootstrapPermissions +} from "@app/ee/services/permission/default-roles"; import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; @@ -32,14 +40,7 @@ import { TGetServiceTokenProjectPermissionArg, TGetUserProjectPermissionArg } from "./permission-service-types"; -import { - buildServiceTokenProjectPermission, - projectAdminPermissions, - projectMemberPermissions, - projectNoAccessPermissions, - ProjectPermissionSet, - projectViewerPermission -} from "./project-permission"; +import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission"; type TPermissionServiceFactoryDep = { orgRoleDAL: Pick; @@ -95,6 +96,10 @@ export const permissionServiceFactory = ({ return projectViewerPermission; case ProjectMembershipRole.NoAccess: return projectNoAccessPermissions; + case ProjectMembershipRole.SshHostBootstrapper: + return sshHostBootstrapPermissions; + case ProjectMembershipRole.KmsCryptographicOperator: + return cryptographicOperatorPermissions; case ProjectMembershipRole.Custom: { return unpackRules>>( permissions as PackRule>>[] diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 993653045..7463c35f6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -678,403 +678,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ export type TProjectPermissionV2Schema = z.infer; -const buildAdminPermissionRules = () => { - const { can, rules } = new AbilityBuilder>(createMongoAbility); - - // Admins get full access to everything - [ - ProjectPermissionSub.SecretFolders, - ProjectPermissionSub.SecretImports, - ProjectPermissionSub.SecretApproval, - ProjectPermissionSub.Role, - ProjectPermissionSub.Integrations, - ProjectPermissionSub.Webhooks, - ProjectPermissionSub.ServiceTokens, - ProjectPermissionSub.Settings, - ProjectPermissionSub.Environments, - ProjectPermissionSub.Tags, - ProjectPermissionSub.AuditLogs, - ProjectPermissionSub.IpAllowList, - ProjectPermissionSub.CertificateAuthorities, - ProjectPermissionSub.CertificateTemplates, - ProjectPermissionSub.PkiAlerts, - ProjectPermissionSub.PkiCollections, - ProjectPermissionSub.SshCertificateAuthorities, - ProjectPermissionSub.SshCertificates, - ProjectPermissionSub.SshCertificateTemplates, - ProjectPermissionSub.SshHostGroups - ].forEach((el) => { - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - el - ); - }); - - can( - [ - ProjectPermissionCertificateActions.Read, - ProjectPermissionCertificateActions.Edit, - ProjectPermissionCertificateActions.Create, - ProjectPermissionCertificateActions.Delete, - ProjectPermissionCertificateActions.ReadPrivateKey - ], - ProjectPermissionSub.Certificates - ); - - can( - [ - ProjectPermissionSshHostActions.Edit, - ProjectPermissionSshHostActions.Read, - ProjectPermissionSshHostActions.Create, - ProjectPermissionSshHostActions.Delete, - ProjectPermissionSshHostActions.IssueHostCert - ], - ProjectPermissionSub.SshHosts - ); - - can( - [ - ProjectPermissionMemberActions.Create, - ProjectPermissionMemberActions.Edit, - ProjectPermissionMemberActions.Delete, - ProjectPermissionMemberActions.Read, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionMemberActions.AssumePrivileges - ], - ProjectPermissionSub.Member - ); - - can( - [ - ProjectPermissionGroupActions.Create, - ProjectPermissionGroupActions.Edit, - ProjectPermissionGroupActions.Delete, - ProjectPermissionGroupActions.Read, - ProjectPermissionGroupActions.GrantPrivileges - ], - ProjectPermissionSub.Groups - ); - - can( - [ - ProjectPermissionIdentityActions.Create, - ProjectPermissionIdentityActions.Edit, - ProjectPermissionIdentityActions.Delete, - ProjectPermissionIdentityActions.Read, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionIdentityActions.AssumePrivileges - ], - ProjectPermissionSub.Identity - ); - - can( - [ - ProjectPermissionSecretActions.DescribeAndReadValue, - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Delete - ], - ProjectPermissionSub.Secrets - ); - - can( - [ - ProjectPermissionDynamicSecretActions.ReadRootCredential, - ProjectPermissionDynamicSecretActions.EditRootCredential, - ProjectPermissionDynamicSecretActions.CreateRootCredential, - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - ProjectPermissionDynamicSecretActions.Lease - ], - ProjectPermissionSub.DynamicSecrets - ); - - can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); - can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); - can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); - can( - [ - ProjectPermissionCmekActions.Create, - ProjectPermissionCmekActions.Edit, - ProjectPermissionCmekActions.Delete, - ProjectPermissionCmekActions.Read, - ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt, - ProjectPermissionCmekActions.Sign, - ProjectPermissionCmekActions.Verify - ], - ProjectPermissionSub.Cmek - ); - can( - [ - ProjectPermissionSecretSyncActions.Create, - ProjectPermissionSecretSyncActions.Edit, - ProjectPermissionSecretSyncActions.Delete, - ProjectPermissionSecretSyncActions.Read, - ProjectPermissionSecretSyncActions.SyncSecrets, - ProjectPermissionSecretSyncActions.ImportSecrets, - ProjectPermissionSecretSyncActions.RemoveSecrets - ], - ProjectPermissionSub.SecretSyncs - ); - - can( - [ - ProjectPermissionKmipActions.CreateClients, - ProjectPermissionKmipActions.UpdateClients, - ProjectPermissionKmipActions.DeleteClients, - ProjectPermissionKmipActions.ReadClients, - ProjectPermissionKmipActions.GenerateClientCertificates - ], - ProjectPermissionSub.Kmip - ); - - can( - [ - ProjectPermissionSecretRotationActions.Create, - ProjectPermissionSecretRotationActions.Edit, - ProjectPermissionSecretRotationActions.Delete, - ProjectPermissionSecretRotationActions.Read, - ProjectPermissionSecretRotationActions.ReadGeneratedCredentials, - ProjectPermissionSecretRotationActions.RotateSecrets - ], - ProjectPermissionSub.SecretRotation - ); - - return rules; -}; - -export const projectAdminPermissions = buildAdminPermissionRules(); - -const buildMemberPermissionRules = () => { - const { can, rules } = new AbilityBuilder>(createMongoAbility); - - can( - [ - ProjectPermissionSecretActions.DescribeAndReadValue, - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Delete - ], - ProjectPermissionSub.Secrets - ); - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.SecretFolders - ); - can( - [ - ProjectPermissionDynamicSecretActions.ReadRootCredential, - ProjectPermissionDynamicSecretActions.EditRootCredential, - ProjectPermissionDynamicSecretActions.CreateRootCredential, - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - ProjectPermissionDynamicSecretActions.Lease - ], - ProjectPermissionSub.DynamicSecrets - ); - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.SecretImports - ); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); - can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); - - can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); - - can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member); - - can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Integrations - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Webhooks - ); - - can( - [ - ProjectPermissionIdentityActions.Read, - ProjectPermissionIdentityActions.Edit, - ProjectPermissionIdentityActions.Create, - ProjectPermissionIdentityActions.Delete - ], - ProjectPermissionSub.Identity - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.ServiceTokens - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Settings - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Environments - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Tags - ); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.Role); - can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs); - can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList); - - // double check if all CRUD are needed for CA and Certificates - can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); - - can( - [ - ProjectPermissionCertificateActions.Read, - ProjectPermissionCertificateActions.Edit, - ProjectPermissionCertificateActions.Create, - ProjectPermissionCertificateActions.Delete - ], - ProjectPermissionSub.Certificates - ); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); - can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); - can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); - can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); - - can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); - - can( - [ - ProjectPermissionCmekActions.Create, - ProjectPermissionCmekActions.Edit, - ProjectPermissionCmekActions.Delete, - ProjectPermissionCmekActions.Read, - ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt, - ProjectPermissionCmekActions.Sign, - ProjectPermissionCmekActions.Verify - ], - ProjectPermissionSub.Cmek - ); - - can( - [ - ProjectPermissionSecretSyncActions.Create, - ProjectPermissionSecretSyncActions.Edit, - ProjectPermissionSecretSyncActions.Delete, - ProjectPermissionSecretSyncActions.Read, - ProjectPermissionSecretSyncActions.SyncSecrets, - ProjectPermissionSecretSyncActions.ImportSecrets, - ProjectPermissionSecretSyncActions.RemoveSecrets - ], - ProjectPermissionSub.SecretSyncs - ); - - return rules; -}; - -export const projectMemberPermissions = buildMemberPermissionRules(); - -const buildViewerPermissionRules = () => { - const { can, rules } = new AbilityBuilder>(createMongoAbility); - - can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); - can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); - can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); - can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity); - can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); - can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); - can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); - can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); - can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); - can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); - - return rules; -}; - -export const projectViewerPermission = buildViewerPermissionRules(); - -const buildNoAccessProjectPermission = () => { - const { rules } = new AbilityBuilder>(createMongoAbility); - return rules; -}; - export const buildServiceTokenProjectPermission = ( scopes: Array<{ secretPath: string; environment: string }>, permission: string[] @@ -1116,8 +719,6 @@ export const buildServiceTokenProjectPermission = ( return build({ conditionsMatcher }); }; -export const projectNoAccessPermissions = buildNoAccessProjectPermission(); - /* eslint-disable */ /** 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 2ca78e876..8e8ebfa13 100644 --- a/backend/src/ee/services/project-template/project-template-fns.ts +++ b/backend/src/ee/services/project-template/project-template-fns.ts @@ -1,22 +1,27 @@ -import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; +import { ProjectType } from "@app/db/schemas"; 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) => ({ +import { ProjectTemplateDefaultEnvironments } from "./project-template-constants"; + +export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ({ id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod + type, 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[] - })), + description: `Infisical's ${type} default project template`, + environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null, + roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map( + ({ name, slug, permissions }) => ({ + name, + slug, + permissions: permissions as TUnpackedPermission[] + }) + ), orgId }); 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 b2430ac14..5b6163977 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -1,10 +1,11 @@ import { ForbiddenError } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import { TProjectTemplates } from "@app/db/schemas"; +import { ProjectType, TProjectTemplates } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { TCreateProjectTemplateDTO, @@ -32,11 +33,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla ...rest, environments: environments as TProjectTemplateEnvironment[], roles: [ - ...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({ - name, - slug, - permissions: permissions as TUnpackedPermission[] - })), + ...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map( + ({ name, slug, permissions }) => ({ + name, + slug, + permissions: permissions as TUnpackedPermission[] + }) + ), ...(roles as TProjectTemplateRole[]).map((role) => ({ ...role, permissions: unpackPermissions(role.permissions) @@ -49,7 +52,7 @@ export const projectTemplateServiceFactory = ({ permissionService, projectTemplateDAL }: TProjectTemplatesServiceFactoryDep) => { - const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => { + const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) @@ -68,11 +71,14 @@ export const projectTemplateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); const projectTemplates = await projectTemplateDAL.find({ - orgId: actor.orgId + orgId: actor.orgId, + ...(type ? { type } : {}) }); return [ - getDefaultProjectTemplate(actor.orgId), + ...(type + ? [getDefaultProjectTemplate(actor.orgId, type)] + : Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))), ...projectTemplates.map((template) => $unpackProjectTemplate(template)) ]; }; @@ -134,7 +140,7 @@ export const projectTemplateServiceFactory = ({ }; const createProjectTemplate = async ( - { roles, environments, ...params }: TCreateProjectTemplateDTO, + { roles, environments, type, ...params }: TCreateProjectTemplateDTO, actor: OrgServiceActor ) => { const plan = await licenseService.getPlan(actor.orgId); @@ -154,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, @@ -169,8 +186,10 @@ export const projectTemplateServiceFactory = ({ const projectTemplate = await projectTemplateDAL.create({ ...params, roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))), - environments: JSON.stringify(environments), - orgId: actor.orgId + environments: + type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null, + orgId: actor.orgId, + type }); return $unpackProjectTemplate(projectTemplate); @@ -202,6 +221,19 @@ export const projectTemplateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); + if (projectTemplate.type !== ProjectType.SecretManager && environments) + throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" }); + + 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/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index c2764dc53..d53b2375e 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { TProjectEnvironments } from "@app/db/schemas"; +import { ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; @@ -15,8 +15,9 @@ export type TProjectTemplateRole = { export type TCreateProjectTemplateDTO = { name: string; description?: string; + type: ProjectType; roles: TProjectTemplateRole[]; - environments: TProjectTemplateEnvironment[]; + environments?: TProjectTemplateEnvironment[] | null; }; export type TUpdateProjectTemplateDTO = Partial; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ae6bbbcab..b6754e323 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1822,8 +1822,12 @@ export const KMS = { }; export const ProjectTemplates = { + LIST: { + type: "The type of project template to list." + }, CREATE: { name: "The name of the project template to be created. Must be slug-friendly.", + type: "The type of project template to be created.", description: "An optional description of the project template.", roles: "The roles to be created when the template is applied to a project.", environments: "The environments to be created when the template is applied to a project." diff --git a/backend/src/services/project-role/project-role-fns.ts b/backend/src/services/project-role/project-role-fns.ts index c465715a7..4dfcf960b 100644 --- a/backend/src/services/project-role/project-role-fns.ts +++ b/backend/src/services/project-role/project-role-fns.ts @@ -1,15 +1,20 @@ -import { ProjectMembershipRole } from "@app/db/schemas"; +import { v4 as uuidv4 } from "uuid"; + +import { ProjectMembershipRole, ProjectType } from "@app/db/schemas"; import { + cryptographicOperatorPermissions, projectAdminPermissions, projectMemberPermissions, projectNoAccessPermissions, - projectViewerPermission -} from "@app/ee/services/permission/project-permission"; + projectViewerPermission, + sshHostBootstrapPermissions +} from "@app/ee/services/permission/default-roles"; +import { TGetPredefinedRolesDTO } from "@app/services/project-role/project-role-types"; -export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => { +export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetPredefinedRolesDTO) => { return [ { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid + id: uuidv4(), projectId, name: "Admin", slug: ProjectMembershipRole.Admin, @@ -19,7 +24,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "Developer", slug: ProjectMembershipRole.Member, @@ -29,7 +34,29 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response + id: uuidv4(), + projectId, + name: "SSH Host Bootstrapper", + slug: ProjectMembershipRole.SshHostBootstrapper, + permissions: sshHostBootstrapPermissions, + description: "Create and issue SSH Hosts in a project", + createdAt: new Date(), + updatedAt: new Date(), + type: ProjectType.SSH + }, + { + id: uuidv4(), + projectId, + name: "Cryptographic Operator", + slug: ProjectMembershipRole.KmsCryptographicOperator, + permissions: cryptographicOperatorPermissions, + description: "Perform cryptographic operations, such as encryption and signing, in a project", + createdAt: new Date(), + updatedAt: new Date(), + type: ProjectType.KMS + }, + { + id: uuidv4(), projectId, name: "Viewer", slug: ProjectMembershipRole.Viewer, @@ -39,7 +66,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember 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, @@ -48,5 +75,5 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember createdAt: new Date(), updatedAt: new Date() } - ].filter(({ slug }) => !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 211dcff4f..babcf7d9c 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -34,7 +34,7 @@ type TProjectRoleServiceFactoryDep = { projectRoleDAL: TProjectRoleDALFactory; identityDAL: Pick; userDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; permissionService: Pick; identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; @@ -98,30 +98,37 @@ export const projectRoleServiceFactory = ({ roleSlug, filter }: TGetRoleDetailsDTO) => { - let projectId = ""; + let project: TProjects; if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: "Project not found" }); - projectId = project.id; + project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); } else { - projectId = filter.projectId; + project = await projectDAL.findProjectById(filter.projectId); } + if (!project) throw new NotFoundError({ message: "Project not found" }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId, + projectId: project.id, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) { - const predefinedRole = getPredefinedRoles(projectId, roleSlug as ProjectMembershipRole)[0]; + const [predefinedRole] = getPredefinedRoles({ + projectId: project.id, + projectType: project.type as ProjectType, + roleFilter: roleSlug as ProjectMembershipRole + }); + + if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` }); + return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) }; } - const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId }); + const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId: project.id }); if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` }); return { ...customRole, permissions: unpackPermissions(customRole.permissions) }; }; @@ -194,29 +201,32 @@ export const projectRoleServiceFactory = ({ }; const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => { - let projectId = ""; + let project: TProjects; if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - if (!project) throw new BadRequestError({ message: "Project not found" }); - projectId = project.id; + project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); } else { - projectId = filter.projectId; + project = await projectDAL.findProjectById(filter.projectId); } + if (!project) throw new BadRequestError({ message: "Project not found" }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId, + projectId: project.id, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); const customRoles = await projectRoleDAL.find( - { projectId }, + { projectId: project.id }, { sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] } ); - const roles = [...getPredefinedRoles(projectId), ...(customRoles || [])]; + const roles = [ + ...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }), + ...(customRoles || []) + ]; return roles; }; diff --git a/backend/src/services/project-role/project-role-types.ts b/backend/src/services/project-role/project-role-types.ts index a71c73113..508623a0c 100644 --- a/backend/src/services/project-role/project-role-types.ts +++ b/backend/src/services/project-role/project-role-types.ts @@ -1,4 +1,4 @@ -import { TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export enum ProjectRoleServiceIdentifierType { @@ -34,3 +34,9 @@ export type TListRolesDTO = { | { type: ProjectRoleServiceIdentifierType.SLUG; projectSlug: string } | { type: ProjectRoleServiceIdentifierType.ID; projectId: string }; } & Omit; + +export type TGetPredefinedRolesDTO = { + projectId: string; + projectType: ProjectType; + roleFilter?: ProjectMembershipRole; +}; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index ecc8c5a36..7ab45baa7 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -329,14 +329,16 @@ export const projectServiceFactory = ({ // set default environments and root folder for provided environments let envs: TProjectEnvironments[] = []; if (projectTemplate) { - envs = await projectEnvDAL.insertMany( - projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })), - tx - ); - await folderDAL.insertMany( - envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })), - tx - ); + if (projectTemplate.environments) { + envs = await projectEnvDAL.insertMany( + projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })), + tx + ); + await folderDAL.insertMany( + envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })), + tx + ); + } await projectRoleDAL.insertMany( projectTemplate.packedRoles.map((role) => ({ ...role, @@ -592,7 +594,10 @@ export const projectServiceFactory = ({ workspaces.map(async (workspace) => { return { ...workspace, - roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)] + roles: [ + ...(workspaceMappedToRoles[workspace.id] || []), + ...getPredefinedRoles({ projectId: workspace.id, projectType: workspace.type as ProjectType }) + ] }; }) ); diff --git a/docs/documentation/platform/project-templates.mdx b/docs/documentation/platform/project-templates.mdx index d84c6bdc1..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 Organization Settings page 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: @@ -67,6 +67,7 @@ In the following steps, we'll explore how to set up a project template. --header 'Content-Type: application/json' \ --data '{ "name": "my-project-template", + "type": "secret-manager", "description": "...", "environments": "[...]", "roles": "[...]", diff --git a/docs/documentation/platform/ssh/overview.mdx b/docs/documentation/platform/ssh/overview.mdx index e71eeabe1..a252e1f71 100644 --- a/docs/documentation/platform/ssh/overview.mdx +++ b/docs/documentation/platform/ssh/overview.mdx @@ -31,16 +31,9 @@ we will register a remote host with Infisical through a [machine identity](/docu - 1.1. Start by creating a new Infisical SSH project in Infisical. + Start by creating a new Infisical SSH project in Infisical. ![ssh project create](/images/platform/ssh/v2/ssh-create-project.png) - - 1.2. Create a custom role in the project under Access Control > Project Roles to grant the machine identity that we will create in step 2 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. - - ![ssh custom role bootstrap 1](/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png) - - ![ssh custom role bootstrap 2](/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png) 2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth. @@ -52,7 +45,14 @@ we will register a remote host with Infisical through a [machine identity](/docu You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth. - 2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the custom role you created in step 1.2. + 2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the **SSH Host Bootstrapper** role. + + 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-add-button.png b/docs/images/platform/project-templates/project-template-add-button.png index 965de1e9a..c71c5c938 100644 Binary files a/docs/images/platform/project-templates/project-template-add-button.png and b/docs/images/platform/project-templates/project-template-add-button.png differ diff --git a/docs/images/platform/project-templates/project-template-apply.png b/docs/images/platform/project-templates/project-template-apply.png index 1ec49cb43..0ed2d320c 100644 Binary files a/docs/images/platform/project-templates/project-template-apply.png and b/docs/images/platform/project-templates/project-template-apply.png differ diff --git a/docs/images/platform/project-templates/project-template-create.png b/docs/images/platform/project-templates/project-template-create.png index 6cd109049..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/docs/images/platform/project-templates/project-template-customized.png b/docs/images/platform/project-templates/project-template-customized.png index f21717326..182e669c2 100644 Binary files a/docs/images/platform/project-templates/project-template-customized.png and b/docs/images/platform/project-templates/project-template-customized.png differ diff --git a/docs/images/platform/project-templates/project-template-edit-form.png b/docs/images/platform/project-templates/project-template-edit-form.png index c4e29297f..72085468f 100644 Binary files a/docs/images/platform/project-templates/project-template-edit-form.png and b/docs/images/platform/project-templates/project-template-edit-form.png differ diff --git a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png deleted file mode 100644 index 8acc1efe9..000000000 Binary files a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png and /dev/null differ diff --git a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png deleted file mode 100644 index 2ad9804d4..000000000 Binary files a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png and /dev/null differ diff --git a/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png b/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png index 83bd3c984..d921cc8d0 100644 Binary files a/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png and b/docs/images/platform/ssh/v2/ssh-add-identity-to-project.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 new file mode 100644 index 000000000..dd08f19a6 Binary files /dev/null and b/frontend/public/images/project-templates/project-templates-new-location.png differ diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index dcd3040d8..c98118a96 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -72,7 +72,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { OrgPermissionSubjects.ProjectTemplates ); - const { data: projectTemplates = [] } = useListProjectTemplates({ + const { data: projectTemplates = [] } = useListProjectTemplates(projectType, { enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates) }); diff --git a/frontend/src/components/projects/ProjectSettings/ProjectSettings.tsx b/frontend/src/components/projects/ProjectSettings/ProjectSettings.tsx new file mode 100644 index 000000000..3919ad86c --- /dev/null +++ b/frontend/src/components/projects/ProjectSettings/ProjectSettings.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; + +import { ProjectTemplatesTab } from "./components"; + +const tabs = [ + { name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab } +]; + +export const ProjectSettings = () => { + const [selectedTab, setSelectedTab] = useState(tabs[0].key); + + return ( + + + {tabs.map((tab) => ( + + {tab.name} + + ))} + + {tabs.map(({ key, component: Component }) => ( + + + + ))} + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx similarity index 92% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx index 98a675940..9b7164f80 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx @@ -7,6 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { usePopUp } from "@app/hooks"; import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal"; import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm"; @@ -24,7 +25,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa "editDetails" ] as const); - const { id: templateId, name, description } = projectTemplate; + const { id: templateId, name, description, type } = projectTemplate; const deleteProjectTemplate = useDeleteProjectTemplate(); @@ -94,10 +95,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa )} - + {type === ProjectType.SecretManager && ( + + )} ; @@ -55,6 +56,8 @@ export const ProjectTemplateEnvironmentsForm = ({ resolver: zodResolver(formSchema) }); + const { subscription } = useSubscription(); + const { fields: environments, move, @@ -67,7 +70,7 @@ export const ProjectTemplateEnvironmentsForm = ({ const onFormSubmit = async (form: TFormSchema) => { try { const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({ - environments: form.environments.map((env, index) => ({ + environments: form.environments?.map((env, index) => ({ ...env, position: index + 1 })), @@ -89,6 +92,9 @@ export const ProjectTemplateEnvironmentsForm = ({ } }; + const isEnvironmentLimitExceeded = + Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit; + return (
{(isAllowed) => ( diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx similarity index 91% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx index e601e0319..5b5728dac 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx @@ -12,6 +12,7 @@ import { ModalContent, TextArea } from "@app/components/v2"; +import { useGetProjectTypeFromRoute } from "@app/hooks"; import { TProjectTemplate, useCreateProjectTemplate, @@ -41,6 +42,7 @@ type FormProps = { const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { const createProjectTemplate = useCreateProjectTemplate(); const updateProjectTemplate = useUpdateProjectTemplate(); + const projectType = useGetProjectTypeFromRoute(); const { handleSubmit, @@ -55,9 +57,17 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { }); const onFormSubmit = async (data: FormData) => { + if (!projectType) { + createNotification({ + text: "Failed to determine project type", + type: "error" + }); + return; + } + const mutation = projectTemplate ? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data }) - : createProjectTemplate.mutateAsync(data); + : createProjectTemplate.mutateAsync({ ...data, type: projectType }); try { const template = await mutation; createNotification({ diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx similarity index 98% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx index d4a076930..ec3f0aaff 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx @@ -50,7 +50,7 @@ export const ProjectTemplatesSection = () => { className="absolute min-h-[10rem] w-full" >
-

+

Create and configure templates with predefined roles and environments to streamline project setup

diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx similarity index 78% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx index b8dc00ab2..7448249ec 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx @@ -23,7 +23,7 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; -import { usePopUp } from "@app/hooks"; +import { useGetProjectTypeFromRoute, usePopUp } from "@app/hooks"; import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal"; @@ -35,8 +35,10 @@ type Props = { export const ProjectTemplatesTable = ({ onEdit }: Props) => { const { subscription } = useSubscription(); - const { isPending, data: projectTemplates = [] } = useListProjectTemplates({ - enabled: subscription?.projectTemplates + const projectType = useGetProjectTypeFromRoute(); + + const { isPending, data: projectTemplates = [] } = useListProjectTemplates(projectType, { + enabled: subscription?.projectTemplates && Boolean(projectType) }); const [search, setSearch] = useState(""); @@ -50,6 +52,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { [search, projectTemplates] ); + const isSecretManagerTemplates = projectType === "secret-manager"; + return (
{ Name Roles - Environments + {isSecretManagerTemplates && Environments} @@ -77,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { /> )} {filteredTemplates.map((template) => { - const { id, name, roles, environments, description } = template; + const { id, name, roles, environments = [], description } = template; return ( onEdit(template)} @@ -116,28 +120,30 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { )} - - {environments.length} - {environments.length > 0 && ( - - {environments - .sort((a, b) => (a.position > b.position ? 1 : -1)) - .map((env) => ( -
  • {env.name}
  • - ))} - - } - > - -
    - )} - + {isSecretManagerTemplates && environments && ( + + {environments.length} + {environments.length > 0 && ( + + {environments + .sort((a, b) => (a.position > b.position ? 1 : -1)) + .map((env) => ( +
  • {env.name}
  • + ))} + + } + > + +
    + )} + + )} {name !== "default" && ( !Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole); -export const formatProjectRoleName = (name: string) => { - if (name === ProjectMemberRole.Member) return "developer"; - return name; -}; - export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -28,3 +16,24 @@ export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) = isCustomOrgRole(roleIdOrSlug) ? roles.find((r) => r.id === roleIdOrSlug) : roles.find((r) => r.slug === roleIdOrSlug); + +export const formatProjectRoleName = (role: string, customRoleName?: string) => { + switch (role) { + case ProjectMembershipRole.Admin: + return "Admin"; + case ProjectMembershipRole.Member: + return "Developer"; + case ProjectMembershipRole.Viewer: + return "Viewer"; + case ProjectMembershipRole.NoAccess: + return "No Access"; + case ProjectMembershipRole.Custom: + return customRoleName ?? role; + case ProjectMembershipRole.SshHostBootstrapper: + return "SSH Host Bootstrapper"; + case ProjectMembershipRole.KmsCryptographicOperator: + return "Cryptographic Operator"; + default: + return role; + } +}; diff --git a/frontend/src/hooks/api/projectTemplates/queries.tsx b/frontend/src/hooks/api/projectTemplates/queries.tsx index f5863915a..89bc96715 100644 --- a/frontend/src/hooks/api/projectTemplates/queries.tsx +++ b/frontend/src/hooks/api/projectTemplates/queries.tsx @@ -6,14 +6,17 @@ import { TProjectTemplate, TProjectTemplateResponse } from "@app/hooks/api/projectTemplates/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; export const projectTemplateKeys = { all: ["project-template"] as const, - list: () => [...projectTemplateKeys.all, "list"] as const, + list: (projectType?: ProjectType) => + [...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const, byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const }; export const useListProjectTemplates = ( + type?: ProjectType, options?: Omit< UseQueryOptions< TProjectTemplate[], @@ -25,9 +28,11 @@ export const useListProjectTemplates = ( > ) => { return useQuery({ - queryKey: projectTemplateKeys.list(), + queryKey: projectTemplateKeys.list(type), queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/project-templates"); + const { data } = await apiRequest.get("/api/v1/project-templates", { + params: { type } + }); return data.projectTemplates; }, diff --git a/frontend/src/hooks/api/projectTemplates/types.ts b/frontend/src/hooks/api/projectTemplates/types.ts index 37c6cc902..9f6ea5ef3 100644 --- a/frontend/src/hooks/api/projectTemplates/types.ts +++ b/frontend/src/hooks/api/projectTemplates/types.ts @@ -1,11 +1,13 @@ import { TProjectRole } from "@app/hooks/api/roles/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; export type TProjectTemplate = { id: string; name: string; + type: ProjectType; description?: string; roles: Pick[]; - environments: { name: string; slug: string; position: number }[]; + environments?: { name: string; slug: string; position: number }[] | null; createdAt: string; updatedAt: string; }; @@ -14,6 +16,7 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] }; export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate }; export type TCreateProjectTemplateDTO = { + type: ProjectType; name: string; description?: string; }; diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index 0a48c9f97..ee95e8c23 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -3,7 +3,9 @@ export enum ProjectMembershipRole { Member = "member", Custom = "custom", Viewer = "viewer", - NoAccess = "no-access" + NoAccess = "no-access", + SshHostBootstrapper = "ssh-host-bootstrapper", + KmsCryptographicOperator = "cryptographic-operator" } export type TGetProjectRolesDTO = { diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index eb4cdbdca..9e5eff713 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,4 +1,5 @@ export { useDebounce } from "./useDebounce"; +export * from "./useGetProjectTypeFromRoute"; export { usePagination } from "./usePagination"; export { usePersistentState } from "./usePersistentState"; export { usePopUp } from "./usePopUp"; diff --git a/frontend/src/hooks/useGetProjectTypeFromRoute.tsx b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx new file mode 100644 index 000000000..95215eff5 --- /dev/null +++ b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx @@ -0,0 +1,13 @@ +import { useRouterState } from "@tanstack/react-router"; + +import { ProjectType } from "@app/hooks/api/workspace/types"; + +export const useGetProjectTypeFromRoute = () => { + const { location } = useRouterState(); + + const segment = location.pathname.split("/")[2]; + + if (!Object.values(ProjectType).includes(segment as ProjectType)) return undefined; + + return segment as ProjectType; +}; diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx index 7cfc21838..6a2382191 100644 --- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -11,11 +11,12 @@ import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2"; import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context"; import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types"; import { usePopUp } from "@app/hooks"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar"; import { SidebarHeader } from "./components/SidebarHeader"; -import { DefaultSideBar, SecretSharingSideBar } from "./ProductsSideBar"; +import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar"; export const OrganizationLayout = () => { const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); @@ -45,21 +46,38 @@ export const OrganizationLayout = () => { ] as string[] ).includes(location.pathname); + const isProjectOverviewOrSettingsPage = ( + [ + linkOptions({ to: "/organization/secret-manager/overview" }).to, + linkOptions({ to: "/organization/secret-manager/settings" }).to, + linkOptions({ to: "/organization/cert-manager/overview" }).to, + linkOptions({ to: "/organization/cert-manager/settings" }).to, + linkOptions({ to: "/organization/kms/overview" }).to, + linkOptions({ to: "/organization/kms/settings" }).to, + linkOptions({ to: "/organization/ssh/overview" }).to, + linkOptions({ to: "/organization/ssh/settings" }).to + ] as string[] + ).includes(location.pathname); + const shouldShowOrgSidebar = location.pathname.startsWith("/organization") && (!isSecretSharingPage || shouldShowProductsSidebar) && - !( - [ - linkOptions({ to: "/organization/secret-manager/overview" }).to, - linkOptions({ to: "/organization/cert-manager/overview" }).to, - linkOptions({ to: "/organization/ssh/overview" }).to, - linkOptions({ to: "/organization/kms/overview" }).to, - linkOptions({ to: "/organization/secret-scanning" }).to - ] as string[] - ).includes(location.pathname); + !([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes( + location.pathname + ); const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen"; + let SideBarComponent = ; + + if (isSecretSharingPage) { + SideBarComponent = ; + } else if (isProjectOverviewOrSettingsPage) { + SideBarComponent = ( + + ); + } + return ( <> @@ -80,10 +98,12 @@ export const OrganizationLayout = () => { className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900" > )} diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/ProjectOverviewSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/ProjectOverviewSideBar.tsx new file mode 100644 index 000000000..4b46cdd0a --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/ProjectOverviewSideBar.tsx @@ -0,0 +1,94 @@ +import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link, useMatchRoute } from "@tanstack/react-router"; + +import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +type TProjectOverviewSideBarProps = { + type: ProjectType; +}; + +export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) => { + const matchRoute = useMatchRoute(); + + const isOverviewActive = !!matchRoute({ + to: `/organization/${type}/overview`, + fuzzy: false + }); + + let label: string; + let icon: string; + let link: string; + + switch (type) { + case ProjectType.CertificateManager: + label = "Cert Management"; + icon = "note"; + link = "https://infisical.com/docs/documentation/platform/pki/overview"; + break; + case ProjectType.SecretManager: + label = "Secret Management"; + icon = "sliding-carousel"; + link = "https://infisical.com/docs/documentation/getting-started/introduction"; + break; + case ProjectType.KMS: + label = "KMS"; + icon = "unlock"; + link = "https://infisical.com/docs/documentation/platform/kms/overview"; + break; + case ProjectType.SSH: + label = "SSH"; + icon = "verified"; + link = "https://infisical.com/docs/documentation/platform/ssh/overview"; + break; + default: + throw new Error("Unknown project type"); + } + + return ( + <> + + + + + + {label} + + + + + + {({ isActive }) => ( + + Settings + + )} + + + + + ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts index b2c79d873..057f5fe6b 100644 --- a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts @@ -1,2 +1,3 @@ export * from "./DefaultSideBar"; +export * from "./ProjectOverviewSideBar"; export * from "./SecretSharingSideBar"; diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index ba0b4f194..8cf807d56 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -269,7 +269,9 @@ export const MinimizedOrgSidebar = () => { @@ -282,7 +284,9 @@ export const MinimizedOrgSidebar = () => { @@ -294,7 +298,8 @@ export const MinimizedOrgSidebar = () => { {({ isActive }) => ( @@ -306,7 +311,8 @@ export const MinimizedOrgSidebar = () => { {({ isActive }) => ( diff --git a/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx b/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx new file mode 100644 index 000000000..c178aca22 --- /dev/null +++ b/frontend/src/pages/organization/CertManagerSettingsPage/CertManagerSettingsPage.tsx @@ -0,0 +1,20 @@ +import { Helmet } from "react-helmet"; + +import { ProjectSettings } from "@app/components/projects/ProjectSettings"; +import { PageHeader } from "@app/components/v2"; + +export const CertManagerSettingsPage = () => { + return ( + <> + + Cert Management Settings + +
    +
    + + +
    +
    + + ); +}; diff --git a/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx b/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx new file mode 100644 index 000000000..aaccee4bf --- /dev/null +++ b/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx @@ -0,0 +1,26 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { CertManagerSettingsPage } from "./CertManagerSettingsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings" +)({ + component: CertManagerSettingsPage, + context: () => ({ + breadcrumbs: [ + { + label: "Products", + icon: () => + }, + { + label: "Cert Management", + link: linkOptions({ to: "/organization/cert-manager/overview" }) + }, + { + label: "Settings" + } + ] + }) +}); diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index 30861a98b..da89d47d8 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -6,9 +6,9 @@ import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2"; +import { formatProjectRoleName } from "@app/helpers/roles"; import { useGetUserWorkspaces } from "@app/hooks/api"; import { IdentityMembership } from "@app/hooks/api/identities/types"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; export enum TabSections { @@ -26,15 +26,6 @@ type Props = { ) => void; }; -const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Admin) return "Admin"; - if (role === ProjectMembershipRole.Member) return "Developer"; - if (role === ProjectMembershipRole.Viewer) return "Viewer"; - if (role === ProjectMembershipRole.NoAccess) return "No Access"; - return role; -}; - export const IdentityProjectRow = ({ membership: { id, createdAt, identity, project, roles }, handlePopUpOpen @@ -80,7 +71,7 @@ export const IdentityProjectRow = ({ {project.type} - {`${formatRoleName(roles[0].role, roles[0].customRoleName)}${ + {`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${ roles.length > 1 ? ` (+${roles.length - 1})` : "" }`} {format(new Date(createdAt), "yyyy-MM-dd")} diff --git a/frontend/src/pages/organization/KmsSettingsPage/KmsSettingsPage.tsx b/frontend/src/pages/organization/KmsSettingsPage/KmsSettingsPage.tsx new file mode 100644 index 000000000..26305fd3d --- /dev/null +++ b/frontend/src/pages/organization/KmsSettingsPage/KmsSettingsPage.tsx @@ -0,0 +1,20 @@ +import { Helmet } from "react-helmet"; + +import { ProjectSettings } from "@app/components/projects/ProjectSettings"; +import { PageHeader } from "@app/components/v2"; + +export const KmsSettingsPage = () => { + return ( + <> + + KMS Settings + +
    +
    + + +
    +
    + + ); +}; diff --git a/frontend/src/pages/organization/KmsSettingsPage/route.tsx b/frontend/src/pages/organization/KmsSettingsPage/route.tsx new file mode 100644 index 000000000..17b0d6687 --- /dev/null +++ b/frontend/src/pages/organization/KmsSettingsPage/route.tsx @@ -0,0 +1,26 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { KmsSettingsPage } from "./KmsSettingsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/kms/settings" +)({ + component: KmsSettingsPage, + context: () => ({ + breadcrumbs: [ + { + label: "Products", + icon: () => + }, + { + label: "KMS", + link: linkOptions({ to: "/organization/kms/overview" }) + }, + { + label: "Settings" + } + ] + }) +}); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx index 50e3a60aa..da93d3cc0 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx @@ -117,12 +117,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }); reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = - error?.response?.data?.message ?? - `Failed to ${popUp?.role?.data ? "update" : "create"} role`; + } catch { + const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`; createNotification({ text, diff --git a/frontend/src/pages/organization/SecretManagerOverviewPage/components/ProjectListToggle.tsx b/frontend/src/pages/organization/SecretManagerOverviewPage/components/ProjectListToggle.tsx index f91d8f578..27b8f4c5b 100644 --- a/frontend/src/pages/organization/SecretManagerOverviewPage/components/ProjectListToggle.tsx +++ b/frontend/src/pages/organization/SecretManagerOverviewPage/components/ProjectListToggle.tsx @@ -27,6 +27,7 @@ export const ProjectListToggle = ({ value, onChange }: Props) => { value={value} onValueChange={onChange} className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0" + dropdownContainerClassName="mt-10 ml-5" > My Projects All Projects diff --git a/frontend/src/pages/organization/SecretManagerSettingsPage/SecretManagerSettingsPage.tsx b/frontend/src/pages/organization/SecretManagerSettingsPage/SecretManagerSettingsPage.tsx new file mode 100644 index 000000000..7db975f4f --- /dev/null +++ b/frontend/src/pages/organization/SecretManagerSettingsPage/SecretManagerSettingsPage.tsx @@ -0,0 +1,20 @@ +import { Helmet } from "react-helmet"; + +import { ProjectSettings } from "@app/components/projects/ProjectSettings"; +import { PageHeader } from "@app/components/v2"; + +export const SecretManagerSettingsPage = () => { + return ( + <> + + Secret Management Settings + +
    +
    + + +
    +
    + + ); +}; diff --git a/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx b/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx new file mode 100644 index 000000000..8e4d6851a --- /dev/null +++ b/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx @@ -0,0 +1,26 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { SecretManagerSettingsPage } from "./SecretManagerSettingsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings" +)({ + component: SecretManagerSettingsPage, + context: () => ({ + breadcrumbs: [ + { + label: "Products", + icon: () => + }, + { + label: "Secret Management", + link: linkOptions({ to: "/organization/secret-manager/overview" }) + }, + { + label: "Settings" + } + ] + }) +}); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 31f8efb0c..bb2afdcc5 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; import { ROUTE_PATHS } from "@app/const/routes"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; @@ -11,7 +12,6 @@ import { OrgAuthTab } from "../OrgAuthTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgGeneralTab } from "../OrgGeneralTab"; import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab"; -import { ProjectTemplatesTab } from "../ProjectTemplatesTab"; export const OrgTabGroup = () => { const search = useSearch({ @@ -28,7 +28,34 @@ export const OrgTabGroup = () => { }, { name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab }, { name: "Import", key: "tab-import", component: ImportTab }, - { name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }, + { + name: "Project Templates", + key: "project-templates", + // scott: temporary, remove once users have adjusted + // eslint-disable-next-line react/no-unstable-nested-components + component: () => ( +
    + +

    + Project templates have been moved to the Feature Settings page, under the "Project + Templates" tab. +

    +

    + Project templates are now product-specific, and can be configured for each project + type. +

    + Project Templates New Location +
    +
    + ) + }, { name: "KMIP", key: "kmip", component: KmipTab } ]; diff --git a/frontend/src/pages/organization/SshSettingsPage/SshSettingsPage.tsx b/frontend/src/pages/organization/SshSettingsPage/SshSettingsPage.tsx new file mode 100644 index 000000000..c720ee087 --- /dev/null +++ b/frontend/src/pages/organization/SshSettingsPage/SshSettingsPage.tsx @@ -0,0 +1,20 @@ +import { Helmet } from "react-helmet"; + +import { ProjectSettings } from "@app/components/projects/ProjectSettings"; +import { PageHeader } from "@app/components/v2"; + +export const SshSettingsPage = () => { + return ( + <> + + SSH Settings + +
    +
    + + +
    +
    + + ); +}; diff --git a/frontend/src/pages/organization/SshSettingsPage/route.tsx b/frontend/src/pages/organization/SshSettingsPage/route.tsx new file mode 100644 index 000000000..c6c65293d --- /dev/null +++ b/frontend/src/pages/organization/SshSettingsPage/route.tsx @@ -0,0 +1,26 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { SshSettingsPage } from "./SshSettingsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings" +)({ + component: SshSettingsPage, + context: () => ({ + breadcrumbs: [ + { + label: "Products", + icon: () => + }, + { + label: "SSH", + link: linkOptions({ to: "/organization/ssh/overview" }) + }, + { + label: "Settings" + } + ] + }) +}); diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectRow.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectRow.tsx index 0fbf4d216..b3581183c 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectRow.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectRow.tsx @@ -5,8 +5,8 @@ import { useNavigate } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2"; +import { formatProjectRoleName } from "@app/helpers/roles"; import { useGetUserWorkspaces } from "@app/hooks/api"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { TWorkspaceUser } from "@app/hooks/api/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { OrgAccessControlTabSections } from "@app/types/org"; @@ -19,15 +19,6 @@ type Props = { ) => void; }; -const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Admin) return "Admin"; - if (role === ProjectMembershipRole.Member) return "Developer"; - if (role === ProjectMembershipRole.Viewer) return "Viewer"; - if (role === ProjectMembershipRole.NoAccess) return "No Access"; - return role; -}; - export const UserProjectRow = ({ membership: { id, project, user, roles }, handlePopUpOpen @@ -73,7 +64,7 @@ export const UserProjectRow = ({ {project.type} - {`${formatRoleName(roles[0].role, roles[0].customRoleName)}${ + {`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${ roles.length > 1 ? ` (+${roles.length - 1})` : "" }`} diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 0af574676..08a17148e 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -24,10 +24,10 @@ import { Tooltip } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api"; import { TGroupMembership } from "@app/hooks/api/groups/types"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; import { groupBy } from "@app/lib/fn/array"; @@ -260,12 +260,6 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember } }; - const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - return role; - }; - return (
    {roles @@ -275,7 +269,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember return (
    -
    {formatRoleName(role, customRoleName)}
    +
    {formatProjectRoleName(role, customRoleName)}
    {isTemporary && (
    @@ -303,7 +297,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember return (
    -
    {formatRoleName(role, customRoleName)}
    +
    {formatProjectRoleName(role, customRoleName)}
    {isTemporary && (
    { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - if (role === ProjectMembershipRole.NoAccess) return "No access"; - return role; -}; export const IdentityTab = withProjectPermission( () => { const { currentWorkspace } = useWorkspace(); @@ -286,7 +280,7 @@ export const IdentityTab = withProjectPermission(
    - {formatRoleName(role, customRoleName)} + {formatProjectRoleName(role, customRoleName)}
    {isTemporary && (
    @@ -331,7 +325,9 @@ export const IdentityTab = withProjectPermission( return (
    -
    {formatRoleName(role, customRoleName)}
    +
    + {formatProjectRoleName(role, customRoleName)} +
    {isTemporary && (
    { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - if (role === ProjectMembershipRole.NoAccess) return "No access"; - return role; -}; type Props = { handlePopUpOpen: ( @@ -343,7 +337,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
    - {formatRoleName(role, customRoleName)} + {formatProjectRoleName(role, customRoleName)}
    {isTemporary && (
    @@ -386,7 +380,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { return (
    -
    {formatRoleName(role, customRoleName)}
    +
    {formatProjectRoleName(role, customRoleName)}
    {isTemporary && (
    { @@ -86,7 +86,9 @@ export const ProjectRoleList = () => { {isRolesLoading && } {roles?.map((role) => { const { id, name, slug } = role; - const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug); + const isNonMutatable = Object.values(ProjectMembershipRole).includes( + slug as ProjectMembershipRole + ); return ( {Object.keys(PROJECT_PERMISSION_OBJECT) + .filter( + (sub) => + ProjectTypePermissionSubjects[currentWorkspace.type][ + sub as ProjectPermissionSub + ] + ) .sort((a, b) => PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title .toLowerCase() diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 2cf1e5567..6b345250c 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -49,6 +49,7 @@ import { isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, + ProjectTypePermissionSubjects, rolePermission2Form } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; @@ -265,6 +266,12 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ {Object.keys(PROJECT_PERMISSION_OBJECT) + .filter( + (subject) => + ProjectTypePermissionSubjects[currentWorkspace.type][ + subject as ProjectPermissionSub + ] + ) .sort((a, b) => PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title .toLowerCase() diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index cca278230..0431726da 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -17,6 +17,7 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { ProjectAccessControlTabs } from "@app/types/project"; @@ -77,7 +78,9 @@ const Page = () => { } }; - const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? ""); + const isCustomRole = !Object.values(ProjectMembershipRole).includes( + (data?.slug ?? "") as ProjectMembershipRole + ); return (
    diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx index ac7978ab6..cb3ac2f85 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx @@ -12,11 +12,14 @@ import { SelectItem } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; +import { useGetProjectTypeFromRoute } from "@app/hooks"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, + ProjectTypePermissionSubjects, TFormSchema } from "./ProjectRoleModifySection.utils"; @@ -37,18 +40,20 @@ export const NewPermissionRule = ({ onClose }: Props) => { .extend({ type: z.nativeEnum(ProjectPermissionSub) }) ), defaultValues: { - type: ProjectPermissionSub.Secrets + type: ProjectPermissionSub.Project } }); const selectedSubject = form.watch("type"); + const projectType = useGetProjectTypeFromRoute(); + return (
    ( )} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index cd7b8cec3..a43674d5d 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -25,6 +25,7 @@ import { TPermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; import { TProjectPermission } from "@app/hooks/api/roles/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; const GeneralPolicyActionSchema = z.object({ read: z.boolean().optional(), @@ -1122,7 +1123,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { ] }, [ProjectPermissionSub.SecretApproval]: { - title: "Secret Protect policy", + title: "Secret Approval Policies", actions: [ { label: "Read", value: "read" }, { label: "Create", value: "create" }, @@ -1195,3 +1196,87 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { ] } }; + +const SharedPermissionSubjects = { + [ProjectPermissionSub.AuditLogs]: true, + [ProjectPermissionSub.Groups]: true, + [ProjectPermissionSub.Member]: true, + [ProjectPermissionSub.Identity]: true, + [ProjectPermissionSub.Project]: true, + [ProjectPermissionSub.Role]: true, + [ProjectPermissionSub.Settings]: true +}; + +const SecretsManagerPermissionSubjects = (enabled = false) => ({ + [ProjectPermissionSub.SecretFolders]: enabled, + [ProjectPermissionSub.SecretImports]: enabled, + [ProjectPermissionSub.DynamicSecrets]: enabled, + [ProjectPermissionSub.Secrets]: enabled, + [ProjectPermissionSub.SecretApproval]: enabled, + [ProjectPermissionSub.Integrations]: enabled, + [ProjectPermissionSub.SecretSyncs]: enabled, + [ProjectPermissionSub.Kms]: enabled, + [ProjectPermissionSub.Environments]: enabled, + [ProjectPermissionSub.Tags]: enabled, + [ProjectPermissionSub.Webhooks]: enabled, + [ProjectPermissionSub.IpAllowList]: enabled, + [ProjectPermissionSub.SecretRollback]: enabled, + [ProjectPermissionSub.SecretRotation]: enabled, + [ProjectPermissionSub.ServiceTokens]: enabled +}); + +const KmsPermissionSubjects = (enabled = false) => ({ + [ProjectPermissionSub.Cmek]: enabled, + [ProjectPermissionSub.Kmip]: enabled +}); + +const CertificateManagerPermissionSubjects = (enabled = false) => ({ + [ProjectPermissionSub.PkiCollections]: enabled, + [ProjectPermissionSub.PkiAlerts]: enabled, + [ProjectPermissionSub.CertificateAuthorities]: enabled, + [ProjectPermissionSub.CertificateTemplates]: enabled, + [ProjectPermissionSub.Certificates]: enabled +}); + +const SshPermissionSubjects = (enabled = false) => ({ + [ProjectPermissionSub.SshCertificateAuthorities]: enabled, + [ProjectPermissionSub.SshCertificates]: enabled, + [ProjectPermissionSub.SshCertificateTemplates]: enabled, + [ProjectPermissionSub.SshHosts]: enabled, + [ProjectPermissionSub.SshHostGroups]: enabled +}); + +// scott: this structure ensures we don't forget to add project permissions to their relevant project type +export const ProjectTypePermissionSubjects: Record< + ProjectType, + Record +> = { + [ProjectType.SecretManager]: { + ...SharedPermissionSubjects, + ...SecretsManagerPermissionSubjects(true), + ...KmsPermissionSubjects(), + ...CertificateManagerPermissionSubjects(), + ...SshPermissionSubjects() + }, + [ProjectType.KMS]: { + ...SharedPermissionSubjects, + ...KmsPermissionSubjects(true), + ...SecretsManagerPermissionSubjects(), + ...CertificateManagerPermissionSubjects(), + ...SshPermissionSubjects() + }, + [ProjectType.CertificateManager]: { + ...SharedPermissionSubjects, + ...CertificateManagerPermissionSubjects(true), + ...KmsPermissionSubjects(), + ...SecretsManagerPermissionSubjects(), + ...SshPermissionSubjects() + }, + [ProjectType.SSH]: { + ...SharedPermissionSubjects, + ...SshPermissionSubjects(true), + ...CertificateManagerPermissionSubjects(), + ...KmsPermissionSubjects(), + ...SecretsManagerPermissionSubjects() + } +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleDetailsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleDetailsSection.tsx index 47a33dd4f..48816709d 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleDetailsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleDetailsSection.tsx @@ -6,6 +6,7 @@ import { IconButton, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useTimedReset } from "@app/hooks"; import { useGetProjectRoleBySlug } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -21,7 +22,9 @@ export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string); - const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? ""); + const isCustomRole = !Object.values(ProjectMembershipRole).includes( + (data?.slug ?? "") as ProjectMembershipRole + ); return data ? (
    diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index 25e33aed4..e97e60edf 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -115,12 +115,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }); reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = - error?.response?.data?.message ?? - `Failed to ${popUp?.role?.data ? "update" : "create"} role`; + } catch { + const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`; createNotification({ text, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 2f46bd065..0331b87c3 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -19,6 +19,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; @@ -31,6 +32,7 @@ import { isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, + ProjectTypePermissionSubjects, rolePermission2Form, TFormSchema } from "./ProjectRoleModifySection.utils"; @@ -104,7 +106,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { } }; - const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? ""); + const isCustomRole = !Object.values(ProjectMembershipRole).includes( + (role?.slug ?? "") as ProjectMembershipRole + ); const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); @@ -192,6 +196,12 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { {Object.keys(PROJECT_PERMISSION_OBJECT) + .filter( + (subject) => + ProjectTypePermissionSubjects[currentWorkspace.type][ + subject as ProjectPermissionSub + ] + ) .sort((a, b) => PROJECT_PERMISSION_OBJECT[ a as keyof typeof PROJECT_PERMISSION_OBJECT @@ -221,17 +231,19 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
    {!isPending && } - {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( - - {renderConditionalComponents(subject, isDisabled)} - - ))} + {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]) + .filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject]) + .map((subject) => ( + + {renderConditionalComponents(subject, isDisabled)} + + ))}
    diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 15d67ab46..5ca262a1d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -52,14 +52,18 @@ import { Route as sshLayoutImport } from './pages/ssh/layout' import { Route as secretManagerLayoutImport } from './pages/secret-manager/layout' import { Route as kmsLayoutImport } from './pages/kms/layout' import { Route as certManagerLayoutImport } from './pages/cert-manager/layout' +import { Route as organizationSshSettingsPageRouteImport } from './pages/organization/SshSettingsPage/route' import { Route as organizationSshOverviewPageRouteImport } from './pages/organization/SshOverviewPage/route' import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route' +import { Route as organizationSecretManagerSettingsPageRouteImport } from './pages/organization/SecretManagerSettingsPage/route' import { Route as organizationSecretManagerOverviewPageRouteImport } from './pages/organization/SecretManagerOverviewPage/route' import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route' import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route' +import { Route as organizationKmsSettingsPageRouteImport } from './pages/organization/KmsSettingsPage/route' import { Route as organizationKmsOverviewPageRouteImport } from './pages/organization/KmsOverviewPage/route' import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route' import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route' +import { Route as organizationCertManagerSettingsPageRouteImport } from './pages/organization/CertManagerSettingsPage/route' import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route' import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route' import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route' @@ -604,6 +608,14 @@ const certManagerLayoutRoute = certManagerLayoutImport.update({ AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRoute, } as any) +const organizationSshSettingsPageRouteRoute = + organizationSshSettingsPageRouteImport.update({ + id: '/ssh/settings', + path: '/ssh/settings', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const organizationSshOverviewPageRouteRoute = organizationSshOverviewPageRouteImport.update({ id: '/ssh/overview', @@ -620,6 +632,14 @@ const organizationSecretSharingSettingsPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute, } as any) +const organizationSecretManagerSettingsPageRouteRoute = + organizationSecretManagerSettingsPageRouteImport.update({ + id: '/secret-manager/settings', + path: '/secret-manager/settings', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const organizationSecretManagerOverviewPageRouteRoute = organizationSecretManagerOverviewPageRouteImport.update({ id: '/secret-manager/overview', @@ -644,6 +664,14 @@ const organizationUserDetailsByIDPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) +const organizationKmsSettingsPageRouteRoute = + organizationKmsSettingsPageRouteImport.update({ + id: '/kms/settings', + path: '/kms/settings', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const organizationKmsOverviewPageRouteRoute = organizationKmsOverviewPageRouteImport.update({ id: '/kms/overview', @@ -668,6 +696,14 @@ const organizationGroupDetailsByIDPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) +const organizationCertManagerSettingsPageRouteRoute = + organizationCertManagerSettingsPageRouteImport.update({ + id: '/cert-manager/settings', + path: '/cert-manager/settings', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const organizationCertManagerOverviewPageRouteRoute = organizationCertManagerOverviewPageRouteImport.update({ id: '/cert-manager/overview', @@ -2072,6 +2108,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationCertManagerOverviewPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings' + path: '/cert-manager/settings' + fullPath: '/organization/cert-manager/settings' + preLoaderRoute: typeof organizationCertManagerSettingsPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': { id: '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId' path: '/groups/$groupId' @@ -2093,6 +2136,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationKmsOverviewPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/kms/settings': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/kms/settings' + path: '/kms/settings' + fullPath: '/organization/kms/settings' + preLoaderRoute: typeof organizationKmsSettingsPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': { id: '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId' path: '/members/$membershipId' @@ -2114,6 +2164,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSecretManagerOverviewPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings' + path: '/secret-manager/settings' + fullPath: '/organization/secret-manager/settings' + preLoaderRoute: typeof organizationSecretManagerSettingsPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': { id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' path: '/settings' @@ -2128,6 +2185,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSshOverviewPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings' + path: '/ssh/settings' + fullPath: '/organization/ssh/settings' + preLoaderRoute: typeof organizationSshSettingsPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout' path: '' @@ -3168,13 +3232,17 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute + organizationCertManagerSettingsPageRouteRoute: typeof organizationCertManagerSettingsPageRouteRoute organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute organizationKmsOverviewPageRouteRoute: typeof organizationKmsOverviewPageRouteRoute + organizationKmsSettingsPageRouteRoute: typeof organizationKmsSettingsPageRouteRoute organizationUserDetailsByIDPageRouteRoute: typeof organizationUserDetailsByIDPageRouteRoute organizationRoleByIDPageRouteRoute: typeof organizationRoleByIDPageRouteRoute organizationSecretManagerOverviewPageRouteRoute: typeof organizationSecretManagerOverviewPageRouteRoute + organizationSecretManagerSettingsPageRouteRoute: typeof organizationSecretManagerSettingsPageRouteRoute organizationSshOverviewPageRouteRoute: typeof organizationSshOverviewPageRouteRoute + organizationSshSettingsPageRouteRoute: typeof organizationSshSettingsPageRouteRoute } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren = @@ -3196,19 +3264,27 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren, organizationCertManagerOverviewPageRouteRoute: organizationCertManagerOverviewPageRouteRoute, + organizationCertManagerSettingsPageRouteRoute: + organizationCertManagerSettingsPageRouteRoute, organizationGroupDetailsByIDPageRouteRoute: organizationGroupDetailsByIDPageRouteRoute, organizationIdentityDetailsByIDPageRouteRoute: organizationIdentityDetailsByIDPageRouteRoute, organizationKmsOverviewPageRouteRoute: organizationKmsOverviewPageRouteRoute, + organizationKmsSettingsPageRouteRoute: + organizationKmsSettingsPageRouteRoute, organizationUserDetailsByIDPageRouteRoute: organizationUserDetailsByIDPageRouteRoute, organizationRoleByIDPageRouteRoute: organizationRoleByIDPageRouteRoute, organizationSecretManagerOverviewPageRouteRoute: organizationSecretManagerOverviewPageRouteRoute, + organizationSecretManagerSettingsPageRouteRoute: + organizationSecretManagerSettingsPageRouteRoute, organizationSshOverviewPageRouteRoute: organizationSshOverviewPageRouteRoute, + organizationSshSettingsPageRouteRoute: + organizationSshSettingsPageRouteRoute, } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = @@ -3893,14 +3969,18 @@ export interface FileRoutesByFullPath { '/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute '/organization/settings/': typeof organizationSettingsPageRouteRoute '/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute + '/organization/cert-manager/settings': typeof organizationCertManagerSettingsPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute + '/organization/kms/settings': typeof organizationKmsSettingsPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute '/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute + '/organization/secret-manager/settings': typeof organizationSecretManagerSettingsPageRouteRoute '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute + '/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute @@ -4072,14 +4152,18 @@ export interface FileRoutesByTo { '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute '/organization/settings': typeof organizationSettingsPageRouteRoute '/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute + '/organization/cert-manager/settings': typeof organizationCertManagerSettingsPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute + '/organization/kms/settings': typeof organizationKmsSettingsPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute '/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute + '/organization/secret-manager/settings': typeof organizationSecretManagerSettingsPageRouteRoute '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute + '/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute @@ -4264,14 +4348,18 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings': typeof organizationCertManagerSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/kms/settings': typeof organizationKmsSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings': typeof organizationSecretManagerSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout': typeof certManagerLayoutRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout': typeof kmsLayoutRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout': typeof secretManagerLayoutRouteWithChildren @@ -4457,14 +4545,18 @@ export interface FileRouteTypes { | '/organization/secret-sharing/' | '/organization/settings/' | '/organization/cert-manager/overview' + | '/organization/cert-manager/settings' | '/organization/groups/$groupId' | '/organization/identities/$identityId' | '/organization/kms/overview' + | '/organization/kms/settings' | '/organization/members/$membershipId' | '/organization/roles/$roleId' | '/organization/secret-manager/overview' + | '/organization/secret-manager/settings' | '/organization/secret-sharing/settings' | '/organization/ssh/overview' + | '/organization/ssh/settings' | '/cert-manager/$projectId/alerting' | '/cert-manager/$projectId/certificate-authorities' | '/cert-manager/$projectId/overview' @@ -4635,14 +4727,18 @@ export interface FileRouteTypes { | '/organization/secret-sharing' | '/organization/settings' | '/organization/cert-manager/overview' + | '/organization/cert-manager/settings' | '/organization/groups/$groupId' | '/organization/identities/$identityId' | '/organization/kms/overview' + | '/organization/kms/settings' | '/organization/members/$membershipId' | '/organization/roles/$roleId' | '/organization/secret-manager/overview' + | '/organization/secret-manager/settings' | '/organization/secret-sharing/settings' | '/organization/ssh/overview' + | '/organization/ssh/settings' | '/cert-manager/$projectId/alerting' | '/cert-manager/$projectId/certificate-authorities' | '/cert-manager/$projectId/overview' @@ -4825,14 +4921,18 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/' | '/_authenticate/_inject-org-details/_org-layout/organization/settings/' | '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview' + | '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings' | '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/organization/kms/overview' + | '/_authenticate/_inject-org-details/_org-layout/organization/kms/settings' | '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview' + | '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' | '/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview' + | '/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout' @@ -5209,13 +5309,17 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview", + "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings", "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/organization/kms/overview", + "/_authenticate/_inject-org-details/_org-layout/organization/kms/settings", "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId", "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview", - "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview" + "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings", + "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview", + "/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings" ] }, "/_authenticate/_inject-org-details/admin/_admin-layout": { @@ -5328,6 +5432,10 @@ export const routeTree = rootRoute "filePath": "organization/CertManagerOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings": { + "filePath": "organization/CertManagerSettingsPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId": { "filePath": "organization/GroupDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" @@ -5340,6 +5448,10 @@ export const routeTree = rootRoute "filePath": "organization/KmsOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/kms/settings": { + "filePath": "organization/KmsSettingsPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId": { "filePath": "organization/UserDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" @@ -5352,6 +5464,10 @@ export const routeTree = rootRoute "filePath": "organization/SecretManagerOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings": { + "filePath": "organization/SecretManagerSettingsPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings": { "filePath": "organization/SecretSharingSettingsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" @@ -5360,6 +5476,10 @@ export const routeTree = rootRoute "filePath": "organization/SshOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings": { + "filePath": "organization/SshSettingsPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout": { "filePath": "cert-manager/layout.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId", diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 3d9889ab3..d1beb5507 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -9,9 +9,13 @@ const adminRoute = route("/admin", [ const organizationRoutes = route("/organization", [ route("/secret-manager/overview", "organization/SecretManagerOverviewPage/route.tsx"), + route("/secret-manager/settings", "organization/SecretManagerSettingsPage/route.tsx"), route("/cert-manager/overview", "organization/CertManagerOverviewPage/route.tsx"), + route("/cert-manager/settings", "organization/CertManagerSettingsPage/route.tsx"), route("/ssh/overview", "organization/SshOverviewPage/route.tsx"), + route("/ssh/settings", "organization/SshSettingsPage/route.tsx"), route("/kms/overview", "organization/KmsOverviewPage/route.tsx"), + route("/kms/settings", "organization/KmsSettingsPage/route.tsx"), route("/access-management", "organization/AccessManagementPage/route.tsx"), route("/admin", "organization/AdminPage/route.tsx"), route("/audit-logs", "organization/AuditLogsPage/route.tsx"),