diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index 33360bf45..4597b64d9 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -171,6 +171,7 @@ ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ +ENV NODE_OPTIONS="--max-old-space-size=1024" WORKDIR /backend diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 6d582ce76..b21eaac09 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true +ENV NODE_OPTIONS="--max-old-space-size=1024" WORKDIR /backend diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 48f52f9e7..f4f251616 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -1,4 +1,8 @@ +import RE2 from "re2"; + import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; export const mockKeyStore = (): TKeyStoreFactory => { @@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, + deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { + const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); + let totalDeleted = 0; + const keys = Object.keys(store); + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + + for (const key of batch) { + if (regex.test(key)) { + delete store[key]; + totalDeleted += 1; + } + } + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, applyJitter(delay, jitter))); + } + + return totalDeleted; + }, getItem: async (key) => { const value = store[key]; if (typeof value === "string") { diff --git a/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts index cb5e44a03..f90b2d593 100644 --- a/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts +++ b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts @@ -3,7 +3,7 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - if (await knex.schema.hasTable(TableName.CertificateBody)) { + if (!(await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain"))) { await knex.schema.alterTable(TableName.CertificateBody, (t) => { t.binary("encryptedCertificateChain").nullable(); }); @@ -25,7 +25,7 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTable(TableName.CertificateSecret); } - if (await knex.schema.hasTable(TableName.CertificateBody)) { + if (await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain")) { await knex.schema.alterTable(TableName.CertificateBody, (t) => { t.dropColumn("encryptedCertificateChain"); }); 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 a07a15d4d..cc78c7327 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -186,11 +186,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/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts index f144a6c00..1bc8e3998 100644 --- a/backend/src/ee/routes/v1/secret-scanning-router.ts +++ b/backend/src/ee/routes/v1/secret-scanning-router.ts @@ -1,11 +1,11 @@ import { z } from "zod"; import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; +import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns"; import { SecretScanningResolvedStatus, SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types"; -import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = body: z.object({ organizationId: z.string().trim() }), response: { 200: z.object({ - sessionId: z.string() + sessionId: z.string(), + gitAppSlug: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const appCfg = getConfig(); - if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) { + if (!canUseSecretScanning(req.auth.orgId)) { throw new BadRequestError({ message: "Secret scanning is temporarily unavailable." }); 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..a3c9c2c11 --- /dev/null +++ b/backend/src/ee/services/permission/default-roles.ts @@ -0,0 +1,462 @@ +import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; + +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionGroupActions, + ProjectPermissionIdentityActions, + ProjectPermissionKmipActions, + ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, + 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( + [ + ProjectPermissionPkiSubscriberActions.Edit, + ProjectPermissionPkiSubscriberActions.Read, + ProjectPermissionPkiSubscriberActions.Create, + ProjectPermissionPkiSubscriberActions.Delete, + ProjectPermissionPkiSubscriberActions.IssueCert, + ProjectPermissionPkiSubscriberActions.ListCerts + ], + ProjectPermissionSub.PkiSubscribers + ); + + 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([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers); + + 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 1c0e9db21..5474facf6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -725,416 +725,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( - [ - ProjectPermissionPkiSubscriberActions.Read, - ProjectPermissionPkiSubscriberActions.Create, - ProjectPermissionPkiSubscriberActions.Edit, - ProjectPermissionPkiSubscriberActions.Delete, - ProjectPermissionPkiSubscriberActions.IssueCert, - ProjectPermissionPkiSubscriberActions.ListCerts - ], - ProjectPermissionSub.PkiSubscribers - ); - - 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([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers); - - 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[] @@ -1176,8 +766,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/ee/services/secret-scanning/secret-scanning-fns.ts b/backend/src/ee/services/secret-scanning/secret-scanning-fns.ts new file mode 100644 index 000000000..b1e2e0bbb --- /dev/null +++ b/backend/src/ee/services/secret-scanning/secret-scanning-fns.ts @@ -0,0 +1,11 @@ +import { getConfig } from "@app/lib/config/env"; + +export const canUseSecretScanning = (orgId: string) => { + const appCfg = getConfig(); + + if (!appCfg.isCloud) { + return true; + } + + return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId); +}; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index c5e7be9d8..7d41091fc 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors"; import { TGitAppDALFactory } from "./git-app-dal"; import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal"; import { TSecretScanningDALFactory } from "./secret-scanning-dal"; +import { canUseSecretScanning } from "./secret-scanning-fns"; import { TSecretScanningQueueFactory } from "./secret-scanning-queue"; import { SecretScanningRiskStatus, @@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({ actorAuthMethod, actorOrgId }: TInstallAppSessionDTO) => { + const appCfg = getConfig(); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const sessionId = crypto.randomBytes(16).toString("hex"); await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId }); - return { sessionId }; + return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG }; }; const linkInstallationToOrg = async ({ @@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({ const { data: { repositories } } = await octokit.apps.listReposAccessibleToInstallation(); - if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) { + + if (canUseSecretScanning(actorOrgId)) { await Promise.all( repositories.map(({ id, full_name }) => secretScanningQueue.startFullRepoScan({ @@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({ ) ); } + return { installatedApp }; }; @@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({ }; const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => { - const appCfg = getConfig(); const { commits, repository, installation, pusher } = payload; if (!commits || !repository || !installation || !pusher) { return; @@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({ }); if (!installationLink) return; - if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) { + if (canUseSecretScanning(installationLink.orgId)) { await secretScanningQueue.startPushEventScan({ commits, pusher: { name: pusher.name, email: pusher.email }, diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index ac28e9ade..6da6c4fa4 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,6 +1,8 @@ import { Redis } from "ioredis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { Redlock, Settings } from "@app/lib/red-lock"; export const PgSqlLock = { @@ -48,6 +50,13 @@ export const KeyStoreTtls = { AccessTokenStatusUpdateInSeconds: 120 }; +type TDeleteItems = { + pattern: string; + batchSize?: number; + delay?: number; + jitter?: number; +}; + type TWaitTillReady = { key: string; waitingCb?: () => void; @@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => { const deleteItem = async (key: string) => redis.del(key); + const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => { + let cursor = "0"; + let totalDeleted = 0; + + do { + // Await in loop is needed so that Redis is not overwhelmed + // eslint-disable-next-line no-await-in-loop + const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads + cursor = nextCursor; + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + const pipeline = redis.pipeline(); + for (const key of batch) { + pipeline.unlink(key); + } + // eslint-disable-next-line no-await-in-loop + await pipeline.exec(); + totalDeleted += batch.length; + console.log("BATCH DONE"); + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, applyJitter(delay, jitter))); + } + } while (cursor !== "0"); + + return totalDeleted; + }; + const incrementBy = async (key: string, value: number) => redis.incrby(key, value); const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds); @@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => { // eslint-disable-next-line await new Promise((resolve) => { waitingCb?.(); - setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); + setTimeout(resolve, Math.max(0, applyJitter(delay, jitter))); }); attempts += 1; // eslint-disable-next-line @@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => { setExpiry, setItemWithExpiry, deleteItem, + deleteItems, incrementBy, acquireLock(resources: string[], duration: number, settings?: Partial) { return redisLock.acquire(resources, duration, settings); diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index 10b28ffec..84cd06c03 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -1,3 +1,7 @@ +import RE2 from "re2"; + +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; import { TKeyStoreFactory } from "./keystore"; @@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, + deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { + const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); + let totalDeleted = 0; + const keys = Object.keys(store); + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + + for (const key of batch) { + if (regex.test(key)) { + delete store[key]; + totalDeleted += 1; + } + } + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, applyJitter(delay, jitter))); + } + + return totalDeleted; + }, getItem: async (key) => { const value = store[key]; if (typeof value === "string") { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 8b8b7abf6..42c956316 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1878,8 +1878,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/lib/config/env.ts b/backend/src/lib/config/env.ts index 907884433..e38dbcfb5 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -146,6 +146,7 @@ const envSchema = z SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()), SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()), SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()), + SECRET_SCANNING_GIT_APP_SLUG: zpStr(z.string().default("infisical-radar")), // LICENSE LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), LICENSE_SERVER_KEY: zpStr(z.string().optional()), diff --git a/backend/src/lib/delay/index.ts b/backend/src/lib/delay/index.ts new file mode 100644 index 000000000..32cb8ebfc --- /dev/null +++ b/backend/src/lib/delay/index.ts @@ -0,0 +1,4 @@ +export const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index ae1a3e821..807d6c286 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -25,6 +25,7 @@ import { TQueueSecretSyncSyncSecretsByIdDTO, TQueueSendSecretSyncActionFailedNotificationsDTO } from "@app/services/secret-sync/secret-sync-types"; +import { CacheType } from "@app/services/super-admin/super-admin-types"; import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; export enum QueueName { @@ -49,7 +50,8 @@ export enum QueueName { AccessTokenStatusUpdate = "access-token-status-update", ImportSecretsFromExternalSource = "import-secrets-from-external-source", AppConnectionSecretSync = "app-connection-secret-sync", - SecretRotationV2 = "secret-rotation-v2" + SecretRotationV2 = "secret-rotation-v2", + InvalidateCache = "invalidate-cache" } export enum QueueJobs { @@ -81,7 +83,8 @@ export enum QueueJobs { SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications", SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", - SecretRotationV2SendNotification = "secret-rotation-v2-send-notification" + SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", + InvalidateCache = "invalidate-cache" } export type TQueueJobTypes = { @@ -234,6 +237,14 @@ export type TQueueJobTypes = { name: QueueJobs.SecretRotationV2SendNotification; payload: TSecretRotationSendNotificationJobPayload; }; + [QueueName.InvalidateCache]: { + name: QueueJobs.InvalidateCache; + payload: { + data: { + type: CacheType; + }; + }; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 681442d1b..42bf37c71 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = { max: 30, // conservative default keyGenerator: (req) => req.realIp }; + +export const invalidateCacheLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + hook: "preValidation", + max: 1, + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f3cac5ab8..f9f586128 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -244,6 +244,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack- import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal"; import { slackServiceFactory } from "@app/services/slack/slack-service"; import { TSmtpService } from "@app/services/smtp/smtp-service"; +import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal"; @@ -613,6 +614,11 @@ export const registerRoutes = async ( queueService }); + const invalidateCacheQueue = invalidateCacheQueueFactory({ + keyStore, + queueService + }); + const userService = userServiceFactory({ userDAL, userAliasDAL, @@ -724,7 +730,8 @@ export const registerRoutes = async ( keyStore, licenseService, kmsService, - microsoftTeamsService + microsoftTeamsService, + invalidateCacheQueue }); const orgAdminService = orgAdminServiceFactory({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index a55aa2ba4..8610a611b 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -4,13 +4,14 @@ import { z } from "zod"; import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; -import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; -import { LoginMethod } from "@app/services/super-admin/super-admin-types"; +import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerAdminRouter = async (server: FastifyZodProvider) => { @@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/invalidate-cache", + config: { + rateLimit: invalidateCacheLimit + }, + schema: { + body: z.object({ + type: z.nativeEnum(CacheType) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + await server.services.superAdmin.invalidateCache(req.body.type); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.InvalidateCache, + distinctId: getTelemetryDistinctId(req), + properties: { + ...req.auditLogInfo + } + }); + + return { + message: "Cache invalidation job started" + }; + } + }); + + server.route({ + method: "GET", + url: "/invalidating-cache-status", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + invalidating: z.boolean() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const invalidating = await server.services.superAdmin.checkIfInvalidatingCache(); + + return { + invalidating + }; + } + }); }; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 4ff3a7ed9..48e144ec8 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -171,7 +171,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .optional() .default(InfisicalProjectTemplate.Default) .describe(PROJECTS.CREATE.template), - type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager) + type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager), + shouldCreateDefaultEnvs: z.boolean().optional().default(true) }), response: { 200: z.object({ @@ -191,7 +192,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, template: req.body.template, - type: req.body.type + type: req.body.type, + createDefaultEnvs: req.body.shouldCreateDefaultEnvs }); await server.services.telemetry.sendPostHogEvents({ @@ -273,7 +275,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ - slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.") + slug: slugSchema({ max: 36 }).describe("The slug of the project to get.") }), response: { 200: projectWithEnv 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 48d907801..9cf94daa7 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -334,14 +334,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, @@ -597,7 +599,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/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index 7e77bd256..abc4dcf82 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -169,7 +169,7 @@ const getParameterStoreTagsRecord = async ( throw new SecretSyncError({ message: - "IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource", + "IAM role has inadequate permissions to manage resource tags. Ensure the following policies are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource", shouldRetry: false }); } diff --git a/backend/src/services/super-admin/invalidate-cache-queue.ts b/backend/src/services/super-admin/invalidate-cache-queue.ts new file mode 100644 index 000000000..c2a12f5d5 --- /dev/null +++ b/backend/src/services/super-admin/invalidate-cache-queue.ts @@ -0,0 +1,49 @@ +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { CacheType } from "./super-admin-types"; + +export type TInvalidateCacheQueueFactoryDep = { + queueService: TQueueServiceFactory; + + keyStore: Pick; +}; + +export type TInvalidateCacheQueueFactory = ReturnType; + +export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => { + const startInvalidate = async (dto: { + data: { + type: CacheType; + }; + }) => { + await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, { + removeOnComplete: true, + removeOnFail: true, + jobId: `invalidate-cache-${dto.data.type}` + }); + }; + + queueService.start(QueueName.InvalidateCache, async (job) => { + try { + const { + data: { type } + } = job.data; + + await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails) + + if (type === CacheType.ALL || type === CacheType.SECRETS) + await keyStore.deleteItems({ pattern: "secret-manager:*" }); + + await keyStore.deleteItem("invalidating-cache"); + } catch (err) { + logger.error(err, "Failed to invalidate cache"); + await keyStore.deleteItem("invalidating-cache"); + } + }); + + return { + startInvalidate + }; +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8687826c8..7c9ca4f38 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; +import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { + CacheType, LoginMethod, TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, @@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = { kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; orgService: Pick; - keyStore: Pick; + keyStore: Pick; licenseService: Pick; microsoftTeamsService: Pick; + invalidateCacheQueue: TInvalidateCacheQueueFactory; }; export type TSuperAdminServiceFactory = ReturnType; @@ -64,7 +67,7 @@ export let getServerCfg: () => Promise< const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s -const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; +export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, @@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({ identityAccessTokenDAL, identityTokenAuthDAL, identityOrgMembershipDAL, - microsoftTeamsService + microsoftTeamsService, + invalidateCacheQueue }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({ await kmsService.updateEncryptionStrategy(strategy); }; + const invalidateCache = async (type: CacheType) => { + await invalidateCacheQueue.startInvalidate({ + data: { type } + }); + }; + + const checkIfInvalidatingCache = async () => { + return (await keyStore.getItem("invalidating-cache")) !== null; + }; + return { initServerCfg, updateServerCfg, @@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({ getConfiguredEncryptionStrategies, grantServerAdminAccessToUser, deleteIdentitySuperAdminAccess, - deleteUserSuperAdminAccess + deleteUserSuperAdminAccess, + invalidateCache, + checkIfInvalidatingCache }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 64ec92632..c804bed74 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -44,3 +44,8 @@ export enum LoginMethod { LDAP = "ldap", OIDC = "oidc" } + +export enum CacheType { + ALL = "all", + SECRETS = "secrets" +} diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 1a2f4061b..a370d0332 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -21,7 +21,8 @@ export enum PostHogEventTypes { IssueSshHostUserCert = "Issue SSH Host User Certificate", IssueSshHostHostCert = "Issue SSH Host Host Certificate", SignCert = "Sign PKI Certificate", - IssueCert = "Issue PKI Certificate" + IssueCert = "Issue PKI Certificate", + InvalidateCache = "Invalidate Cache" } export type TSecretModifiedEvent = { @@ -205,6 +206,13 @@ export type TIssueCertificateEvent = { }; }; +export type TInvalidateCacheEvent = { + event: PostHogEventTypes.InvalidateCache; + properties: { + userAgent?: string; + }; +}; + export type TPostHogEvent = { distinctId: string } & ( | TSecretModifiedEvent | TAdminInitEvent @@ -223,4 +231,5 @@ export type TPostHogEvent = { distinctId: string } & ( | TIssueSshHostHostCertEvent | TSignCertificateEvent | TIssueCertificateEvent + | TInvalidateCacheEvent ); diff --git a/docs/api-reference/endpoints/certificates/bundle.mdx b/docs/api-reference/endpoints/certificates/bundle.mdx index 5fbda7d96..60d37a2d8 100644 --- a/docs/api-reference/endpoints/certificates/bundle.mdx +++ b/docs/api-reference/endpoints/certificates/bundle.mdx @@ -1,6 +1,6 @@ --- title: "Get Certificate Bundle" -openapi: "GET /api/v2/workspace/{slug}/bundle" +openapi: "GET /api/v1/pki/certificates/{serialNumber}/bundle" --- diff --git a/docs/api-reference/endpoints/certificates/private-key.mdx b/docs/api-reference/endpoints/certificates/private-key.mdx index 244aecea3..d0b93e65c 100644 --- a/docs/api-reference/endpoints/certificates/private-key.mdx +++ b/docs/api-reference/endpoints/certificates/private-key.mdx @@ -1,4 +1,4 @@ --- title: "Get Certificate Private Key" -openapi: "GET /api/v2/workspace/{slug}/private-key" +openapi: "GET /api/v1/pki/certificates/{serialNumber}/private-key" --- diff --git a/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx index 3f62a3b61..b6d1d691f 100644 --- a/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx +++ b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx @@ -27,7 +27,7 @@ User identities can have metadata attributes assigned directly. These attributes #### Applying ABAC Policies with User Metadata -Attribute-based access controls are currently only available for polices defined on Secrets Manager projects. +Attribute-based access controls are currently only available for policies defined on Secrets Manager projects. You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags. 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/secret-scanning.mdx b/docs/documentation/platform/secret-scanning.mdx index 4f030e882..da28bfa55 100644 --- a/docs/documentation/platform/secret-scanning.mdx +++ b/docs/documentation/platform/secret-scanning.mdx @@ -7,6 +7,113 @@ The Infisical Secret Scanner allows you to keep an overview and stay alert of ex To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes. + + + + To setup secret scanning on your own instance of Infisical, you can follow the steps below. + + + + Create a new GitHub app in your GitHub organization or personal [Developer Settings](https://github.com/settings/apps). + + ![Create GitHub App](/images/platform/secret-scanning/github-create-app.png) + + ### Configure the GitHub App + To configure the GitHub app to work with Infisical, you'll need to modify the following settings: + - **Homepage URL**: Required to be set. Set it to the URL of your Infisical instance. (e.g. `https://app.infisical.com`) + - **Setup URL**: Set this to `https:///organization/secret-scanning` + - **Webhook URL**: Set this to `https:///api/v1/secret-scanning/webhook` + - **Webhook Secret**: Set this to a random string. This is used to verify the webhook request from Infisical. Use `openssl rand -base64 32` in your terminal to generate a random secret. + + + Remember to save the webhook secret as you will need it in the next step. + + + ![GitHub App Settings](/images/platform/secret-scanning/github-configure-app.png) + + ### Configure the GitHub App Permissions + The GitHub app needs the following permissions: + + Repository permissions: + - `Checks`: Read and Write + - `Contents`: Read-only + - `Issues`: Read and Write + - `Pull Requests`: Read and Write + - `Metadata`: Read-only (enabled by default) + + ![Github App Repository Permissions](/images/platform/secret-scanning/github-repo-permissions.png) + + Subscribed events: + - `Check run` + - `Pull request` + - `Push` + + ![Github App Subscribed Events](/images/platform/secret-scanning/github-subscribed-events.png) + + + ### Create the GitHub App + Now you can create the GitHub app by clicking on the "Create GitHub App" button. + + + If you want other Github users to be able to install the app, you need to tick the "Any account" option under "Where can this GitHub App be installed?" + + + ![Create GitHub App](/images/platform/secret-scanning/github-create-app-button.png) + + + + After clicking the "Create GitHub App" button, you will be redirected to the GitHub settings page. Here you can copy the "App ID" and save it for later when you need to configure your environment variables for your Infisical instance. + + ![Github App ID](/images/platform/secret-scanning/github-app-copy-app-id.png) + + + + The GitHub App slug is the name of the app you created in a slug friendly format. You can find the slug in the URL of the app you created. + + ![Github App Slug](/images/platform/secret-scanning/github-app-copy-slug.png) + + + + Create a new app private key by clicking on the "Generate a private key" button under the "Private keys" section. + + Once you click the "Generate a private key" button, the private key will be downloaded to your computer. Save this file for later as you will need the private key when configuring Infisical. + + ![Github App Private Key](/images/platform/secret-scanning/github-app-create-private-key.png) + + + Remember to save the private key as you will need it in the next step. + + + + + + + Now you can configure your Infisical instance by setting the following environment variables: + + - `SECRET_SCANNING_GIT_APP_ID`: The App ID of your GitHub App. + - `SECRET_SCANNING_GIT_APP_SLUG`: The slug of your GitHub App. + - `SECRET_SCANNING_PRIVATE_KEY`: The private key of your GitHub App that you created in a previous step. + - `SECRET_SCANNING_WEBHOOK_SECRET`: The webhook secret of your GitHub App that you created in a previous step. + + + + After restarting your Infisical instance, you should be able to use the secret scanning feature within your organization. Follow the steps below to add the GitHub App to your Infisical organization. + + +## Install the Infisical Radar GitHub App + +To install the GitHub App, press the "Integrate With GitHub" button in the top right corner of your Infisical Secret Scanning dashboard. + +![Integrate With GitHub](/images/platform/secret-scanning/infisical-connect-secret-scanner.png) + +Next, you'll be prompted to select which organization you'd like to install the app into. Select the organization you'd like to install the app into by clicking the organization in the menu. + +![Select Organization](/images/platform/secret-scanning/github-select-org-2.png) + +Select the repositories you'd like to scan for secrets and press the "Install" button. + +![Select Repositories](/images/platform/secret-scanning/github-select-repos.png) + ## Code Scanning ![Scanning Overview](/images/platform/secret-scanning/overview.png) 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/secret-scanning/github-app-copy-app-id.png b/docs/images/platform/secret-scanning/github-app-copy-app-id.png new file mode 100644 index 000000000..a94cb5ece Binary files /dev/null and b/docs/images/platform/secret-scanning/github-app-copy-app-id.png differ diff --git a/docs/images/platform/secret-scanning/github-app-copy-slug.png b/docs/images/platform/secret-scanning/github-app-copy-slug.png new file mode 100644 index 000000000..c555dcd41 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-app-copy-slug.png differ diff --git a/docs/images/platform/secret-scanning/github-app-create-private-key.png b/docs/images/platform/secret-scanning/github-app-create-private-key.png new file mode 100644 index 000000000..50f602a36 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-app-create-private-key.png differ diff --git a/docs/images/platform/secret-scanning/github-configure-app.png b/docs/images/platform/secret-scanning/github-configure-app.png new file mode 100644 index 000000000..df64eeb18 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-configure-app.png differ diff --git a/docs/images/platform/secret-scanning/github-create-app-button.png b/docs/images/platform/secret-scanning/github-create-app-button.png new file mode 100644 index 000000000..3ea4b2d38 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-create-app-button.png differ diff --git a/docs/images/platform/secret-scanning/github-create-app.png b/docs/images/platform/secret-scanning/github-create-app.png new file mode 100644 index 000000000..f4d1cdb8c Binary files /dev/null and b/docs/images/platform/secret-scanning/github-create-app.png differ diff --git a/docs/images/platform/secret-scanning/github-register-app.png b/docs/images/platform/secret-scanning/github-register-app.png new file mode 100644 index 000000000..904c07bf2 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-register-app.png differ diff --git a/docs/images/platform/secret-scanning/github-repo-permissions.png b/docs/images/platform/secret-scanning/github-repo-permissions.png new file mode 100644 index 000000000..53eae9a41 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-repo-permissions.png differ diff --git a/docs/images/platform/secret-scanning/github-select-org-2.png b/docs/images/platform/secret-scanning/github-select-org-2.png new file mode 100644 index 000000000..55b945c18 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-select-org-2.png differ diff --git a/docs/images/platform/secret-scanning/github-select-org.png b/docs/images/platform/secret-scanning/github-select-org.png new file mode 100644 index 000000000..7d6e5abc5 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-select-org.png differ diff --git a/docs/images/platform/secret-scanning/github-select-repos.png b/docs/images/platform/secret-scanning/github-select-repos.png new file mode 100644 index 000000000..51a6648d2 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-select-repos.png differ diff --git a/docs/images/platform/secret-scanning/github-subscribed-events.png b/docs/images/platform/secret-scanning/github-subscribed-events.png new file mode 100644 index 000000000..7aa6b431f Binary files /dev/null and b/docs/images/platform/secret-scanning/github-subscribed-events.png differ diff --git a/docs/images/platform/secret-scanning/infisical-connect-secret-scanner.png b/docs/images/platform/secret-scanning/infisical-connect-secret-scanner.png new file mode 100644 index 000000000..11f24fd74 Binary files /dev/null and b/docs/images/platform/secret-scanning/infisical-connect-secret-scanner.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/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 21f54994a..5962e4c10 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -165,7 +165,7 @@ spec: - Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. + Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. #### Available options diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index 50f07bb76..d87648bbf 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec metadata: name: infisical-push-secret-demo spec: - resyncInterval: 1m + resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD. hostAPI: https://app.infisical.com/api # Optional, defaults to no replacement. @@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y - The `resyncInterval` is a string-formatted duration that defines the time between each resync. + The `resyncInterval` is a string-formatted duration that defines the time between each resync. The field is optional, and will default to no automatic resync if not defined. + + If you don't want to automatically reconcile the InfisicalPushSecret CRD on an interval, you can remove the `resyncInterval` field entirely from your InfisicalPushSecret CRD. The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. @@ -239,7 +241,21 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y DATABASE_URL: postgres://127.0.0.1:5432 ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab ``` + + + The `generators[]` field is used to define the generators you want to use for your InfisicalPushSecret CRD. + You can follow the guide for [using generators to push secrets](#using-generators-to-push-secrets) for more information. + Example: + + ```yaml + push: + generators: + - destinationSecretName: password-generator-test + generatorRef: + kind: Password + name: password-generator + ``` @@ -459,6 +475,148 @@ Using Go templates, you can format, combine, and create new key-value pairs of s Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. +## Using generators to push secrets + +Generators allow secrets to be dynamically generated during each reconciliation cycle and then pushed to Infisical. They are useful for use cases where a new secret value is needed on every sync, such as ephemeral credentials or one-time-use tokens. + +A generator is defined as a custom resource (`ClusterGenerator`) within the cluster, which specifies the logic for generating secret values. Generators are stateless, each invocation triggers the creation of a new set of values, with no tracking or persistence of previously generated data. + +Because of this behavior, you may want to disable automatic syncing for the `InfisicalPushSecret` resource to avoid continuous regeneration of secrets. This can be done by omitting the `resyncInterval` field from the InfisicalPushSecret CRD. + +### Example usage +```yaml + push: + secret: + secretName: push-secret-source-secret + secretNamespace: dev + generators: + - destinationSecretName: password-generator # Name of the secret that will be created in Infisical + generatorRef: + kind: Password # Kind of the resource, must match the generator kind. + name: custom-generator # Name of the generator resource +``` + +To use a generator, you must specify at least one generator in the `push.generators[]` field. + + + + This field holds an array of the generators you want to use for your InfisicalPushSecret CRD. + + + + The name of the secret that will be created in Infisical. + + + + The reference to the generator resource. + + Valid fields: + - `kind`: The kind of the generator resource, must match the generator kind. + - `name`: The name of the generator resource. + + + + The kind of the generator resource, must match the generator kind. + + Valid values: + - `Password` + - `UUID` + + + + The name of the generator resource. + + +### Supported Generators +Below are the currently supported generators for the InfisicalPushSecret CRD. Each generator is a `ClusterGenerator` custom resource that can be used to customize the generated secret. + + + ### Password Generator + + The Password generator is a custom resource that is installed on the cluster that defines the logic for generating a password. + - `kind`: The kind of the generator resource, must match the generator kind. For the Password generator, the kind is `Password`. + - `generator.passwordSpec`: The spec of the password generator. + + + The `generator.kind` field must match the kind of the generator resource. For the Password generator, the kind should always be set to `Password`. + + + - `length`: The length of the password. + - `digits`: The number of digits in the password. + - `symbols`: The number of symbols in the password. + - `symbolCharacters`: The characters to use for the symbols in the password. + - `noUpper`: Whether to include uppercase letters in the password. + - `allowRepeat`: Whether to allow repeating characters in the password. + + + ```yaml password-cluster-generator.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: ClusterGenerator + metadata: + name: password-generator + spec: + kind: Password + generator: + passwordSpec: + length: 10 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true + ``` + + Example InfisicalPushSecret CRD using the Password generator: + ```yaml infisical-push-secret-crd.yaml + push: + generators: + - destinationSecretName: password-generator-test + generatorRef: + kind: Password + name: password-generator + ``` + + + ### UUID Generator + + The UUID generator is a custom resource that is installed on the cluster that defines the logic for generating a UUID. + - `kind`: The kind of the generator resource, must match the generator kind. For the UUID generator, the kind is `UUID`. + - `generator.uuidSpec`: The spec of the UUID generator. For UUID's, this can be left empty. + + + The `generator.kind` field must match the kind of the generator resource. For the UUID generator, the kind should always be set to `UUID`. + + + + The spec of the UUID generator. For UUID's, this can be left empty. + + + ```yaml uuid-cluster-generator.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: ClusterGenerator + metadata: + name: uuid-generator + spec: + kind: UUID + generator: + uuidSpec: + ``` + + Example InfisicalPushSecret CRD using the UUID generator: + + ```yaml infisical-push-secret-crd.yaml + push: + generators: + - destinationSecretName: uuid-generator-test + generatorRef: + kind: UUID + name: uuid-generator + ``` + + + + + ## Applying the InfisicalPushSecret CRD to your cluster Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index a79a8d0a5..5ff5d468f 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -832,7 +832,7 @@ The namespace of the managed Kubernetes secret to be created. Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. -Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. +Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. #### Available options @@ -940,7 +940,7 @@ The Infisical operator will automatically create the Kubernetes config map in th The namespace of the managed Kubernetes config map that your Infisical data will be stored in. - Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. + Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. #### Available options diff --git a/docs/internals/bug-bounty.mdx b/docs/internals/bug-bounty.mdx index c5bc8b39e..e45de05bf 100644 --- a/docs/internals/bug-bounty.mdx +++ b/docs/internals/bug-bounty.mdx @@ -58,3 +58,23 @@ We ask that researchers: - Give us a reasonable window to investigate and patch before going public Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally. + +### Program Conduct and Enforcement + +We value professional and collaborative interaction with security researchers. To maintain the integrity of our bug bounty program, we expect all participants to adhere to the following guidelines: + +- Maintain professional communication in all interactions +- Do not threaten public disclosure of vulnerabilities before we've had reasonable time to investigate and address the issue +- Do not attempt to extort or coerce compensation through threats +- Follow the responsible disclosure process outlined in this document +- Do not use automated scanning tools without prior permission + +Violations of these guidelines may result in: + +1. **Warning**: For minor violations, we may issue a warning explaining the violation and requesting compliance with program guidelines. +2. **Temporary Ban**: Repeated minor violations or more serious violations may result in a temporary suspension from the program. +3. **Permanent Ban**: Severe violations such as threats, extortion attempts, or unauthorized public disclosure will result in permanent removal from the Infisical Bug Bounty Program. + +We reserve the right to reject reports, withhold bounties, and remove participants from the program at our discretion for conduct that undermines the collaborative spirit of security research. + +Infisical is committed to working respectfully with security researchers who follow these guidelines, and we strive to recognize and reward valuable contributions that help protect our platform and users. diff --git a/docs/mint.json b/docs/mint.json index c34ce945d..317f110e6 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -1467,6 +1467,8 @@ "api-reference/endpoints/certificates/revoke", "api-reference/endpoints/certificates/delete", "api-reference/endpoints/certificates/cert-body", + "api-reference/endpoints/certificates/bundle", + "api-reference/endpoints/certificates/private-key", "api-reference/endpoints/certificates/issue-certificate", "api-reference/endpoints/certificates/sign-certificate" ] diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index ae7c4ab33..b63c58d3a 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -625,6 +625,26 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I +## Secret Scanning + + + + The App ID of your GitHub App. + + + + The slug of your GitHub App. + + + + A private key for your GitHub App. + + + + The webhook secret of your GitHub App. + + + ## Observability You can configure Infisical to collect and expose telemetry data for analytics and monitoring. 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 && ( + + )} { - const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); const formMethods = useForm({ values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, @@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({ - handlePopUpToggle("createPolicy", isOpen)} + - - - handlePopUpToggle("createPolicy")} /> - - + Add Policies + + handlePopUpToggle("addPolicy", isOpen)} + /> )} diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx similarity index 93% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx index b72d12c73..ef2691d69 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx @@ -19,7 +19,7 @@ import { THead, Tr } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { slugSchema } from "@app/lib/schemas"; @@ -35,6 +35,7 @@ const formSchema = z.object({ slug: slugSchema({ min: 1, max: 32 }) }) .array() + .nullish() }); type TFormSchema = z.infer; @@ -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/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index bc812e18e..5bedf1158 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -3,6 +3,7 @@ export { useAdminGrantServerAdminAccess, useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, + useInvalidateCache, useRemoveUserServerAdminAccess, useUpdateServerConfig, useUpdateServerEncryptionStrategy diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 2c88d4fd8..f220573c9 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { RootKeyEncryptionStrategy, TCreateAdminUserDTO, + TInvalidateCacheDTO, TServerConfig, TUpdateServerConfigDTO } from "./types"; @@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => { } }); }; + +export const useInvalidateCache = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (dto) => { + await apiRequest.post("/api/v1/admin/invalidate-cache", dto); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() }); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 1d44a93d0..85c6c153e 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -8,6 +8,7 @@ import { AdminGetIdentitiesFilters, AdminGetUsersFilters, AdminIntegrationsConfig, + TGetInvalidatingCacheStatus, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -22,8 +23,10 @@ export const adminQueryKeys = { getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, getIdentities: (filters: AdminGetIdentitiesFilters) => [adminStandaloneKeys.getIdentities, { filters }] as const, - getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const, - getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const + getAdminSlackConfig: () => ["admin-slack-config"] as const, + getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, + getInvalidateCache: () => ["admin-invalidate-cache"] as const, + getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const }; export const fetchServerConfig = async () => { @@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => { } }); }; + +export const useGetInvalidatingCacheStatus = (enabled = true) => { + return useQuery({ + queryKey: adminQueryKeys.getInvalidateCache(), + queryFn: async () => { + const { data } = await apiRequest.get( + "/api/v1/admin/invalidating-cache-status" + ); + + return data.invalidating; + }, + enabled, + refetchInterval: (data) => (data ? 3000 : false) + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 4533b5963..8850b3375 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -24,6 +24,7 @@ export type TServerConfig = { enabledLoginMethods: LoginMethod[]; authConsentContent?: string; pageFrameContent?: string; + invalidatingCache: boolean; }; export type TUpdateServerConfigDTO = { @@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } + +export enum CacheType { + ALL = "all", + SECRETS = "secrets" +} + +export type TInvalidateCacheDTO = { + type: CacheType; +}; + +export type TGetInvalidatingCacheStatus = { + invalidating: boolean; +}; 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/api/secretScanning/mutation.ts b/frontend/src/hooks/api/secretScanning/mutation.ts index 7298b9af1..5055da4aa 100644 --- a/frontend/src/hooks/api/secretScanning/mutation.ts +++ b/frontend/src/hooks/api/secretScanning/mutation.ts @@ -10,15 +10,17 @@ import { } from "./types"; export const useCreateNewInstallationSession = () => { - return useMutation<{ sessionId: string }, object, { organizationId: string }>({ - mutationFn: async (opt) => { - const { data } = await apiRequest.post( - "/api/v1/secret-scanning/create-installation-session/organization", - opt - ); - return data; + return useMutation<{ sessionId: string; gitAppSlug: string }, object, { organizationId: string }>( + { + mutationFn: async (opt) => { + const { data } = await apiRequest.post( + "/api/v1/secret-scanning/create-installation-session/organization", + opt + ); + return data; + } } - }); + ); }; export const useUpdateRiskStatus = () => { 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..b3b8f3d5e --- /dev/null +++ b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx @@ -0,0 +1,22 @@ +import { useMemo } from "react"; +import { useRouterState } from "@tanstack/react-router"; + +import { ProjectType } from "@app/hooks/api/workspace/types"; + +export const useGetProjectTypeFromRoute = () => { + const { location } = useRouterState(); + + return useMemo(() => { + const segments = location.pathname.split("/"); + + let type: ProjectType | undefined; + + // location of project type can vary in router path, so we need to check all possible values + segments.forEach((segment) => { + if (Object.values(ProjectType).includes(segment as ProjectType)) + type = segment as ProjectType; + }); + + return type; + }, [location]); +}; 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/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index af93b3c2a..790adff50 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -31,6 +31,7 @@ import { import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel"; import { AuthPanel } from "./components/AuthPanel"; +import { CachingPanel } from "./components/CachingPanel"; import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { UserPanel } from "./components/UserPanel"; @@ -42,7 +43,8 @@ enum TabSections { Integrations = "integrations", Users = "users", Identities = "identities", - Kmip = "kmip" + Kmip = "kmip", + Caching = "caching" } enum SignUpModes { @@ -164,6 +166,7 @@ export const OverviewPage = () => { Integrations User Identities Machine Identities + Caching
    @@ -408,6 +411,9 @@ export const OverviewPage = () => { + + +
    )} diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx new file mode 100644 index 000000000..170a85c0d --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react"; +import { faRotate } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Badge, Button, DeleteActionModal } from "@app/components/v2"; +import { useUser } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useInvalidateCache } from "@app/hooks/api"; +import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries"; +import { CacheType } from "@app/hooks/api/admin/types"; + +export const CachingPanel = () => { + const { mutateAsync: invalidateCache } = useInvalidateCache(); + const { user } = useUser(); + + const [type, setType] = useState(null); + const [shouldPoll, setShouldPoll] = useState(false); + + const { + data: invalidationStatus, + isFetching, + refetch + } = useGetInvalidatingCacheStatus(shouldPoll); + const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus)); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "invalidateCache" + ] as const); + + const handleInvalidateCacheSubmit = async () => { + if (!type || isInvalidating) return; + + try { + await invalidateCache({ type }); + createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); + setShouldPoll(true); + handlePopUpClose("invalidateCache"); + } catch (err) { + console.error(err); + createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" }); + } + }; + + useEffect(() => { + if (isInvalidating) return; + + if (shouldPoll) { + setShouldPoll(false); + createNotification({ text: "Successfully invalidated cache", type: "success" }); + } + }, [isInvalidating, shouldPoll]); + + useEffect(() => { + refetch().then((v) => setShouldPoll(v.data || false)); + }, []); + + return ( + <> +
    +
    +
    + Secrets Cache + {isInvalidating && ( + + + Invalidating Cache + + )} +
    + + The encrypted secrets cache encompasses all secrets stored within the system and + provides a temporary, secure storage location for frequently accessed credentials. + +
    + + +
    + handlePopUpToggle("invalidateCache", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleInvalidateCacheSubmit} + /> + + ); +}; 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/SecretScanningPage/SecretScanningPage.tsx b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx index ba919cdd3..23cdcf800 100644 --- a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx @@ -108,7 +108,7 @@ export const SecretScanningPage = withPermission( const generateNewIntegrationSession = async () => { const session = await createNewIntegrationSession({ organizationId }); - window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`; + window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`; }; return ( 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 ( { const [newToken, setToken] = useState(""); const [isTokenCopied, setIsTokenCopied] = useToggle(false); - const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?.id ?? ""); const createServiceToken = useCreateServiceToken(); const hasServiceToken = Boolean(newToken); @@ -118,26 +113,13 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => { try { if (!currentWorkspace?.id) return; - if (!latestFileKey) return; - - const key = decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY") as string - }); const randomBytes = crypto.randomBytes(16).toString("hex"); - const { ciphertext, iv, tag } = encryptSymmetric({ - plaintext: key, - key: randomBytes - }); - const { serviceToken } = await createServiceToken.mutateAsync({ - encryptedKey: ciphertext, - iv, - tag, + encryptedKey: "", + iv: "", + tag: "", scopes, expiresIn: Number(expiresIn), name, diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index 8557519c9..50d392748 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -18,10 +18,6 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, FormControl, FormLabel, Input, @@ -37,6 +33,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; import { useCreateIdentityProjectAdditionalPrivilege, useGetIdentityProjectPrivilegeDetails, @@ -45,9 +42,9 @@ import { import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, - isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, rolePermission2Form @@ -100,6 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); const form = useForm({ values: privilegeDetails @@ -194,30 +192,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ } } - const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { - const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); - if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [...rootPolicyValue, {}], - { shouldDirty: true, shouldTouch: true } - ); - } else { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [{}], - { - shouldDirty: true, - shouldTouch: true - } - ); - } - }; - return ( } > Save - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((permissionSubject) => ( - onNewPolicy(permissionSubject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title} - - ))} - - +
    @@ -429,6 +382,10 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ ) )}
    + handlePopUpToggle("addPolicy", isOpen)} + /> ); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 2cf1e5567..5fee21f9c 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -17,10 +17,6 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, FormControl, FormLabel, Input, @@ -36,6 +32,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; import { useCreateProjectUserAdditionalPrivilege, useGetProjectUserPrivilegeDetails, @@ -44,9 +41,9 @@ import { import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, - isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, rolePermission2Form @@ -86,6 +83,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ projectMembershipId, isDisabled }: Props) => { + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); + const isCreate = !privilegeId; const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -166,30 +165,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ } }; - const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { - const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); - if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [...rootPolicyValue, {}], - { shouldDirty: true, shouldTouch: true } - ); - } else { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [{}], - { - shouldDirty: true, - shouldTouch: true - } - ); - } - }; - const privilegeTemporaryAccess = form.watch("temporaryAccess"); const isTemporary = privilegeTemporaryAccess?.isTemporary; const isExpired = @@ -245,46 +220,25 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((subject) => ( - onNewPolicy(subject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} - - ))} - - +
    @@ -423,6 +377,10 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ ))}
    + handlePopUpToggle("addPolicy", isOpen)} + /> ); 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 deleted file mode 100644 index ac7978ab6..000000000 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Controller, useForm, useFormContext } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { - Button, - Checkbox, - FormControl, - FormLabel, - ModalClose, - Select, - SelectItem -} from "@app/components/v2"; -import { ProjectPermissionSub } from "@app/context"; - -import { - isConditionalSubjects, - PROJECT_PERMISSION_OBJECT, - projectRoleFormSchema, - TFormSchema -} from "./ProjectRoleModifySection.utils"; - -type Props = { - onClose: () => void; -}; - -export const NewPermissionRule = ({ onClose }: Props) => { - const rootForm = useFormContext(); - - const form = useForm<{ - type: ProjectPermissionSub; - permissions: NonNullable; - }>({ - resolver: zodResolver( - projectRoleFormSchema - .pick({ permissions: true }) - .extend({ type: z.nativeEnum(ProjectPermissionSub) }) - ), - defaultValues: { - type: ProjectPermissionSub.Secrets - } - }); - - const selectedSubject = form.watch("type"); - - return ( -
    - ( - - - - )} - /> - -
    - {PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => ( - ( -
    - - {label} - -
    - )} - /> - ))} -
    -
    - - - - -
    -
    - ); -}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx new file mode 100644 index 000000000..e636bdfe4 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx @@ -0,0 +1,214 @@ +import { useState } from "react"; +import { Controller, useForm, useFormContext } from "react-hook-form"; +import { faCheck, faSearch, faXmark, faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + EmptyState, + IconButton, + Input, + Modal, + ModalClose, + ModalContent, + Table, + TableContainer, + TBody, + Td, + Tooltip, + Tr +} 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, + ProjectTypePermissionSubjects, + TFormSchema +} from "./ProjectRoleModifySection.utils"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onClose: () => void; +}; + +type TForm = { permissions: Record }; + +const Content = ({ onClose }: ContentProps) => { + const rootForm = useFormContext(); + const [search, setSearch] = useState(""); + const { + control, + handleSubmit, + formState: { isDirty }, + setValue, + reset + } = useForm({ + defaultValues: { + permissions: Object.fromEntries( + Object.values(ProjectPermissionSub).map((subject) => [subject, false]) + ) + } + }); + + const projectType = useGetProjectTypeFromRoute(); + + const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT) + .filter( + ([subject, { title }]) => + ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][ + subject as ProjectPermissionSub + ] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true) + ) + .sort((a, b) => a[1].title.localeCompare(b[1].title)) + .map(([subject]) => subject); + + const onSubmit = () => + handleSubmit((form) => { + Object.entries(form.permissions).forEach(([subject, add]) => { + if (!add) return; + + const type = subject as ProjectPermissionSub; + + const rootPolicyValue = rootForm.getValues("permissions")?.[type]; + + if (rootPolicyValue && isConditionalSubjects(subject as ProjectPermissionSub)) { + rootForm.setValue( + `permissions.${type}`, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error akhilmhdh: this is because of ts collision with both + [...rootPolicyValue, {}], + { shouldDirty: true, shouldTouch: true } + ); + } else if (!rootPolicyValue?.length) { + rootForm.setValue( + `permissions.${type}`, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error akhilmhdh: this is because of ts collision with both + [{}], + { + shouldDirty: true, + shouldTouch: true + } + ); + } + }); + onClose(); + })(); + + return ( + <> + setSearch(e.target.value)} + leftIcon={} + rightIcon={ + search ? ( + setSearch("")}> + + + ) : null + } + /> + +
    + Resource +
    + + + reset()} + variant="plain" + size="xs" + className={`text-mineshaft-400 ${!isDirty ? "pointer-events-none opacity-50" : ""} hover:text-red`} + isDisabled={!isDirty} + > + + + +
    +
    + + + {filteredPolicies.map((subject) => ( + ( + onChange(!value)} + > + + + + )} + name={`permissions.${subject as ProjectPermissionSub}`} + /> + ))} + +
    + {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} + + {value ? : null} +
    + {!filteredPolicies.length && ( + + )} +
    +
    + + + + +
    + + ); +}; + +export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 8c0e19ec4..35ab714a9 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -26,6 +26,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(), @@ -957,7 +958,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { actions: [{ label: "Modify", value: "edit" }] }, [ProjectPermissionSub.Integrations]: { - title: "Integrations", + title: "Native Integrations", actions: [ { label: "Read", value: "read" }, { label: "Create", value: "create" }, @@ -1178,7 +1179,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" }, @@ -1251,3 +1252,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 42223e884..547ee638a 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -8,18 +8,15 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { AccessTree } from "@app/components/permissions"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/v2"; +import { Button } from "@app/components/v2"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; +import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; @@ -32,6 +29,7 @@ import { isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, + ProjectTypePermissionSubjects, rolePermission2Form, TFormSchema } from "./ProjectRoleModifySection.utils"; @@ -93,6 +91,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { mutateAsync: updateRole } = useUpdateProjectRole(); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); + const onSubmit = async (el: TFormSchema) => { try { if (!projectId || !role?.id) return; @@ -109,31 +109,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { } }; - const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? ""); - - const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { - const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); - if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [...rootPolicyValue, {}], - { shouldDirty: true, shouldTouch: true } - ); - } else { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [{}], - { - shouldDirty: true, - shouldTouch: true - } - ); - } - }; + const isCustomRole = !Object.values(ProjectMembershipRole).includes( + (role?.slug ?? "") as ProjectMembershipRole + ); const permissions = form.watch("permissions"); @@ -177,48 +155,25 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[ - a as keyof typeof PROJECT_PERMISSION_OBJECT - ].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((subject) => ( - onNewPolicy(subject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} - - ))} - - +
    )} @@ -226,18 +181,24 @@ 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)} + + ))}
    + handlePopUpToggle("addPolicy", isOpen)} + />
    diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 269d77543..a9304e7ed 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -251,7 +251,7 @@ export const AccessPolicyForm = ({ label="Policy Type" isRequired isError={Boolean(error)} - tooltipText="Change polices govern secret changes within a given environment and secret path. Access polices allow underprivileged user to request access to environment/secret path." + tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path." errorText={error?.message} > )} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 4f850c944..76ee2256a 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' @@ -610,6 +614,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', @@ -626,6 +638,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', @@ -650,6 +670,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', @@ -674,6 +702,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', @@ -2103,6 +2139,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' @@ -2124,6 +2167,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' @@ -2145,6 +2195,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' @@ -2159,6 +2216,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: '' @@ -3220,13 +3284,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 = @@ -3248,19 +3316,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 = @@ -3966,14 +4042,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 @@ -4148,14 +4228,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 @@ -4342,14 +4426,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 @@ -4538,14 +4626,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' @@ -4719,14 +4811,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' @@ -4911,14 +5007,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' @@ -5298,13 +5398,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": { @@ -5417,6 +5521,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" @@ -5429,6 +5537,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" @@ -5441,6 +5553,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" @@ -5449,6 +5565,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 37234310d..b9eeb6392 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"), diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index c8bfc3539..e22529f9c 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.9.1 +version: v0.9.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.9.1" +appVersion: "v0.9.2" diff --git a/helm-charts/secrets-operator/templates/clustergenerator-crd.yaml b/helm-charts/secrets-operator/templates/clustergenerator-crd.yaml new file mode 100644 index 000000000..8da166a5e --- /dev/null +++ b/helm-charts/secrets-operator/templates/clustergenerator-crd.yaml @@ -0,0 +1,97 @@ +{{- if .Values.installCRDs }} +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clustergenerators.secrets.infisical.com + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + labels: + {{- include "secrets-operator.labels" . | nindent 4 }} +spec: + group: secrets.infisical.com + names: + kind: ClusterGenerator + listKind: ClusterGeneratorList + plural: clustergenerators + singular: clustergenerator + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterGenerator represents a cluster-wide generator + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + generator: + description: Generator the spec for this generator, must match the kind. + properties: + passwordSpec: + description: PasswordSpec controls the behavior of the password + generator. + properties: + allowRepeat: + default: false + description: set allowRepeat to true to allow repeating characters. + type: boolean + digits: + description: digits specifies the number of digits in the generated + password. If omitted it defaults to 25% of the length of the + password + type: integer + length: + default: 24 + description: Length of the password to be generated. Defaults + to 24 + type: integer + noUpper: + default: false + description: Set noUpper to disable uppercase characters + type: boolean + symbolCharacters: + description: symbolCharacters specifies the special characters + that should be used in the generated password. + type: string + symbols: + description: symbols specifies the number of symbol characters + in the generated password. If omitted it defaults to 25% of + the length of the password + type: integer + type: object + uuidSpec: + description: UUIDSpec controls the behavior of the uuid generator. + type: object + type: object + kind: + description: Kind the kind of this generator. + enum: + - Password + - UUID + type: string + required: + - kind + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +{{- end }} diff --git a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml index 5afb4f519..64ddc3e5e 100644 --- a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml @@ -143,6 +143,34 @@ spec: type: string push: properties: + generators: + items: + properties: + destinationSecretName: + type: string + generatorRef: + properties: + kind: + allOf: + - enum: + - Password + - UUID + - enum: + - Password + - UUID + description: Specify the Kind of the generator resource + type: string + name: + type: string + required: + - kind + - name + type: object + required: + - destinationSecretName + - generatorRef + type: object + type: array secret: properties: secretName: @@ -168,8 +196,6 @@ spec: - secretName - secretNamespace type: object - required: - - secret type: object resyncInterval: type: string @@ -199,7 +225,6 @@ spec: required: - destination - push - - resyncInterval type: object status: description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret diff --git a/helm-charts/secrets-operator/templates/manager-rbac.yaml b/helm-charts/secrets-operator/templates/manager-rbac.yaml index 3ab1a7409..93289ca67 100644 --- a/helm-charts/secrets-operator/templates/manager-rbac.yaml +++ b/helm-charts/secrets-operator/templates/manager-rbac.yaml @@ -81,6 +81,18 @@ rules: - tokenreviews verbs: - create +- apiGroups: + - secrets.infisical.com + resources: + - clustergenerators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - secrets.infisical.com resources: diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index cb96f3959..78cfe4ad7 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: v0.9.1 + tag: v0.9.2 resources: limits: cpu: 500m diff --git a/k8-operator/api/v1alpha1/generators.go b/k8-operator/api/v1alpha1/generators.go new file mode 100644 index 000000000..0f6d86c2d --- /dev/null +++ b/k8-operator/api/v1alpha1/generators.go @@ -0,0 +1,152 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GeneratorKind represents a kind of generator. +// +kubebuilder:validation:Enum=Password;UUID +type GeneratorKind string + +const ( + GeneratorKindPassword GeneratorKind = "Password" + GeneratorKindUUID GeneratorKind = "UUID" +) + +type ClusterGeneratorSpec struct { + // Kind the kind of this generator. + Kind GeneratorKind `json:"kind"` + + // Generator the spec for this generator, must match the kind. + Generator GeneratorSpec `json:"generator,omitempty"` +} + +type GeneratorSpec struct { + // +kubebuilder:validation:Optional + PasswordSpec *PasswordSpec `json:"passwordSpec,omitempty"` + // +kubebuilder:validation:Optional + UUIDSpec *UUIDSpec `json:"uuidSpec,omitempty"` +} + +// ClusterGenerator represents a cluster-wide generator +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +type ClusterGenerator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterGeneratorSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterGeneratorList contains a list of ClusterGenerator resources. +type ClusterGeneratorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterGenerator `json:"items"` +} + +// ! UUID Generator + +// UUIDSpec controls the behavior of the uuid generator. +type UUIDSpec struct{} + +// UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216). +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type UUID struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec UUIDSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// UUIDList contains a list of UUID resources. +type UUIDList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []UUID `json:"items"` +} + +// ! Password Generator + +// PasswordSpec controls the behavior of the password generator. +type PasswordSpec struct { + // Length of the password to be generated. + // Defaults to 24 + // +kubebuilder:validation:Optional + // +kubebuilder:default=24 + Length int `json:"length"` + + // digits specifies the number of digits in the generated + // password. If omitted it defaults to 25% of the length of the password + Digits *int `json:"digits,omitempty"` + + // symbols specifies the number of symbol characters in the generated + // password. If omitted it defaults to 25% of the length of the password + Symbols *int `json:"symbols,omitempty"` + + // symbolCharacters specifies the special characters that should be used + // in the generated password. + SymbolCharacters *string `json:"symbolCharacters,omitempty"` + + // Set noUpper to disable uppercase characters + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + NoUpper bool `json:"noUpper"` + + // set allowRepeat to true to allow repeating characters. + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + AllowRepeat bool `json:"allowRepeat"` +} + +// Password generates a random password based on the +// configuration parameters in spec. +// You can specify the length, characterset and other attributes. +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced +type Password struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PasswordSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// PasswordList contains a list of Password resources. +type PasswordList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Password `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Password{}, &PasswordList{}) + SchemeBuilder.Register(&UUID{}, &UUIDList{}) + SchemeBuilder.Register(&ClusterGenerator{}, &ClusterGeneratorList{}) +} diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go index 5a7438881..8958c714d 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -29,9 +29,28 @@ type InfisicalPushSecretSecretSource struct { Template *SecretTemplate `json:"template,omitempty"` } -type SecretPush struct { +type GeneratorRef struct { + // Specify the Kind of the generator resource + // +kubebuilder:validation:Enum=Password;UUID // +kubebuilder:validation:Required - Secret InfisicalPushSecretSecretSource `json:"secret"` + Kind GeneratorKind `json:"kind"` + + // +kubebuilder:validation:Required + Name string `json:"name"` +} + +type SecretPushGenerator struct { + // +kubebuilder:validation:Required + DestinationSecretName string `json:"destinationSecretName"` + // +kubebuilder:validation:Required + GeneratorRef GeneratorRef `json:"generatorRef"` +} + +type SecretPush struct { + // +kubebuilder:validation:Optional + Secret *InfisicalPushSecretSecretSource `json:"secret,omitempty"` + // +kubebuilder:validation:Optional + Generators []SecretPushGenerator `json:"generators,omitempty"` } // InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret @@ -52,7 +71,8 @@ type InfisicalPushSecretSpec struct { // +kubebuilder:validation:Required Push SecretPush `json:"push"` - ResyncInterval string `json:"resyncInterval"` + // +kubebuilder:validation:Optional + ResyncInterval *string `json:"resyncInterval,omitempty"` // Infisical host to pull secrets from // +kubebuilder:validation:Optional diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index 4958e9c76..382b0e8cd 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -96,6 +96,80 @@ func (in *CaReference) DeepCopy() *CaReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGenerator. +func (in *ClusterGenerator) DeepCopy() *ClusterGenerator { + if in == nil { + return nil + } + out := new(ClusterGenerator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterGenerator) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterGeneratorList) DeepCopyInto(out *ClusterGeneratorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterGenerator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorList. +func (in *ClusterGeneratorList) DeepCopy() *ClusterGeneratorList { + if in == nil { + return nil + } + out := new(ClusterGeneratorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterGeneratorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterGeneratorSpec) DeepCopyInto(out *ClusterGeneratorSpec) { + *out = *in + in.Generator.DeepCopyInto(&out.Generator) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorSpec. +func (in *ClusterGeneratorSpec) DeepCopy() *ClusterGeneratorSpec { + if in == nil { + return nil + } + out := new(ClusterGeneratorSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DynamicSecretDetails) DeepCopyInto(out *DynamicSecretDetails) { *out = *in @@ -143,6 +217,46 @@ func (in *GcpIamAuthDetails) DeepCopy() *GcpIamAuthDetails { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GeneratorRef) DeepCopyInto(out *GeneratorRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorRef. +func (in *GeneratorRef) DeepCopy() *GeneratorRef { + if in == nil { + return nil + } + out := new(GeneratorRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) { + *out = *in + if in.PasswordSpec != nil { + in, out := &in.PasswordSpec, &out.PasswordSpec + *out = new(PasswordSpec) + (*in).DeepCopyInto(*out) + } + if in.UUIDSpec != nil { + in, out := &in.UUIDSpec, &out.UUIDSpec + *out = new(UUIDSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec. +func (in *GeneratorSpec) DeepCopy() *GeneratorSpec { + if in == nil { + return nil + } + out := new(GeneratorSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GenericAwsIamAuth) DeepCopyInto(out *GenericAwsIamAuth) { *out = *in @@ -483,6 +597,11 @@ func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) { out.Destination = in.Destination in.Authentication.DeepCopyInto(&out.Authentication) in.Push.DeepCopyInto(&out.Push) + if in.ResyncInterval != nil { + in, out := &in.ResyncInterval, &out.ResyncInterval + *out = new(string) + **out = **in + } out.TLS = in.TLS } @@ -746,10 +865,107 @@ func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Password) DeepCopyInto(out *Password) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Password. +func (in *Password) DeepCopy() *Password { + if in == nil { + return nil + } + out := new(Password) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Password) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PasswordList) DeepCopyInto(out *PasswordList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Password, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordList. +func (in *PasswordList) DeepCopy() *PasswordList { + if in == nil { + return nil + } + out := new(PasswordList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PasswordList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PasswordSpec) DeepCopyInto(out *PasswordSpec) { + *out = *in + if in.Digits != nil { + in, out := &in.Digits, &out.Digits + *out = new(int) + **out = **in + } + if in.Symbols != nil { + in, out := &in.Symbols, &out.Symbols + *out = new(int) + **out = **in + } + if in.SymbolCharacters != nil { + in, out := &in.SymbolCharacters, &out.SymbolCharacters + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordSpec. +func (in *PasswordSpec) DeepCopy() *PasswordSpec { + if in == nil { + return nil + } + out := new(PasswordSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretPush) DeepCopyInto(out *SecretPush) { *out = *in - in.Secret.DeepCopyInto(&out.Secret) + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(InfisicalPushSecretSecretSource) + (*in).DeepCopyInto(*out) + } + if in.Generators != nil { + in, out := &in.Generators, &out.Generators + *out = make([]SecretPushGenerator, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush. @@ -762,6 +978,22 @@ func (in *SecretPush) DeepCopy() *SecretPush { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretPushGenerator) DeepCopyInto(out *SecretPushGenerator) { + *out = *in + out.GeneratorRef = in.GeneratorRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPushGenerator. +func (in *SecretPushGenerator) DeepCopy() *SecretPushGenerator { + if in == nil { + return nil + } + out := new(SecretPushGenerator) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) { *out = *in @@ -848,6 +1080,79 @@ func (in *TLSConfig) DeepCopy() *TLSConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UUID) DeepCopyInto(out *UUID) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUID. +func (in *UUID) DeepCopy() *UUID { + if in == nil { + return nil + } + out := new(UUID) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UUID) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UUIDList) DeepCopyInto(out *UUIDList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]UUID, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDList. +func (in *UUIDList) DeepCopy() *UUIDList { + if in == nil { + return nil + } + out := new(UUIDList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UUIDList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UUIDSpec) DeepCopyInto(out *UUIDSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDSpec. +func (in *UUIDSpec) DeepCopy() *UUIDSpec { + if in == nil { + return nil + } + out := new(UUIDSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) { *out = *in diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_clustergenerators.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_clustergenerators.yaml new file mode 100644 index 000000000..c4a9eb168 --- /dev/null +++ b/k8-operator/config/crd/bases/secrets.infisical.com_clustergenerators.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: clustergenerators.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: ClusterGenerator + listKind: ClusterGeneratorList + plural: clustergenerators + singular: clustergenerator + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterGenerator represents a cluster-wide generator + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + generator: + description: Generator the spec for this generator, must match the + kind. + properties: + passwordSpec: + description: PasswordSpec controls the behavior of the password + generator. + properties: + allowRepeat: + default: false + description: set allowRepeat to true to allow repeating characters. + type: boolean + digits: + description: digits specifies the number of digits in the + generated password. If omitted it defaults to 25% of the + length of the password + type: integer + length: + default: 24 + description: Length of the password to be generated. Defaults + to 24 + type: integer + noUpper: + default: false + description: Set noUpper to disable uppercase characters + type: boolean + symbolCharacters: + description: symbolCharacters specifies the special characters + that should be used in the generated password. + type: string + symbols: + description: symbols specifies the number of symbol characters + in the generated password. If omitted it defaults to 25% + of the length of the password + type: integer + type: object + uuidSpec: + description: UUIDSpec controls the behavior of the uuid generator. + type: object + type: object + kind: + description: Kind the kind of this generator. + enum: + - Password + - UUID + type: string + required: + - kind + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index 37df72854..9d7e2adbd 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -142,6 +142,34 @@ spec: type: string push: properties: + generators: + items: + properties: + destinationSecretName: + type: string + generatorRef: + properties: + kind: + allOf: + - enum: + - Password + - UUID + - enum: + - Password + - UUID + description: Specify the Kind of the generator resource + type: string + name: + type: string + required: + - kind + - name + type: object + required: + - destinationSecretName + - generatorRef + type: object + type: array secret: properties: secretName: @@ -168,8 +196,6 @@ spec: - secretName - secretNamespace type: object - required: - - secret type: object resyncInterval: type: string @@ -200,7 +226,6 @@ spec: required: - destination - push - - resyncInterval type: object status: description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_passwords.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_passwords.yaml new file mode 100644 index 000000000..dc14f2bf0 --- /dev/null +++ b/k8-operator/config/crd/bases/secrets.infisical.com_passwords.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: passwords.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: Password + listKind: PasswordList + plural: passwords + singular: password + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Password generates a random password based on the configuration + parameters in spec. You can specify the length, characterset and other attributes. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PasswordSpec controls the behavior of the password generator. + properties: + allowRepeat: + default: false + description: set allowRepeat to true to allow repeating characters. + type: boolean + digits: + description: digits specifies the number of digits in the generated + password. If omitted it defaults to 25% of the length of the password + type: integer + length: + default: 24 + description: Length of the password to be generated. Defaults to 24 + type: integer + noUpper: + default: false + description: Set noUpper to disable uppercase characters + type: boolean + symbolCharacters: + description: symbolCharacters specifies the special characters that + should be used in the generated password. + type: string + symbols: + description: symbols specifies the number of symbol characters in + the generated password. If omitted it defaults to 25% of the length + of the password + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_uuids.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_uuids.yaml new file mode 100644 index 000000000..495b5b276 --- /dev/null +++ b/k8-operator/config/crd/bases/secrets.infisical.com_uuids.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: uuids.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: UUID + listKind: UUIDList + plural: uuids + singular: uuid + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216). + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: UUIDSpec controls the behavior of the uuid generator. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/k8-operator/config/crd/kustomization.yaml b/k8-operator/config/crd/kustomization.yaml index ea6db574a..02e3fad98 100644 --- a/k8-operator/config/crd/kustomization.yaml +++ b/k8-operator/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/secrets.infisical.com_infisicalsecrets.yaml - bases/secrets.infisical.com_infisicalpushsecrets.yaml - bases/secrets.infisical.com_infisicaldynamicsecrets.yaml + - bases/secrets.infisical.com_clustergenerators.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/k8-operator/config/rbac/role.yaml b/k8-operator/config/rbac/role.yaml index 542face87..ea2fbada3 100644 --- a/k8-operator/config/rbac/role.yaml +++ b/k8-operator/config/rbac/role.yaml @@ -74,6 +74,18 @@ rules: - tokenreviews verbs: - create +- apiGroups: + - secrets.infisical.com + resources: + - clustergenerators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - secrets.infisical.com resources: diff --git a/k8-operator/config/samples/crd/pushsecret/cluster-password-generator.yml b/k8-operator/config/samples/crd/pushsecret/cluster-password-generator.yml new file mode 100644 index 000000000..ce3c8c087 --- /dev/null +++ b/k8-operator/config/samples/crd/pushsecret/cluster-password-generator.yml @@ -0,0 +1,14 @@ +apiVersion: secrets.infisical.com/v1alpha1 +kind: ClusterGenerator +metadata: + name: password-generator +spec: + kind: Password + generator: + passwordSpec: + length: 10 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index 861880d95..6e3b135e4 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -393,7 +393,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c // Max TTL if infisicalDynamicSecret.Status.MaxTTL != "" { - maxTTLDuration, err := util.ConvertIntervalToDuration(infisicalDynamicSecret.Status.MaxTTL) + maxTTLDuration, err := util.ConvertIntervalToDuration(&infisicalDynamicSecret.Status.MaxTTL) if err != nil { return defaultNextReconcile, fmt.Errorf("unable to parse MaxTTL duration: %w", err) } diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go index ebf537a63..47c55d698 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -51,7 +51,7 @@ func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger //+kubebuilder:rbac:groups="",resources=pods,verbs=get;list //+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create //+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create - +// +kubebuilder:rbac:groups=secrets.infisical.com,resources=clustergenerators,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // For more details, check Reconcile and its Result here: @@ -108,23 +108,30 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. return ctrl.Result{}, nil } - if infisicalPushSecretCRD.Spec.ResyncInterval != "" { + if infisicalPushSecretCRD.Spec.Push.Secret == nil && infisicalPushSecretCRD.Spec.Push.Generators == nil { + logger.Info("No secret or generators found, skipping reconciliation. Please define ") + return ctrl.Result{}, nil + } - duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval) + duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval) - if err != nil { + if err != nil { + // if resyncInterval is nil, we don't want to reconcile automatically + if infisicalPushSecretCRD.Spec.ResyncInterval != nil { logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ RequeueAfter: requeueTime, }, nil + } else { + logger.Error(err, "unable to convert resync interval to duration") + return ctrl.Result{}, err } + } - requeueTime = duration + requeueTime = duration + if requeueTime != 0 { logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime)) - - } else { - logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime)) } // Check if the resource is already marked for deletion @@ -137,10 +144,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. // Get modified/default config infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client) if err != nil { - logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime)) - return ctrl.Result{ - RequeueAfter: requeueTime, - }, nil + if requeueTime != 0 { + logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } else { + logger.Error(err, "unable to fetch infisical-config") + return ctrl.Result{}, err + } } if infisicalPushSecretCRD.Spec.HostAPI == "" { @@ -152,10 +164,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" { api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCRD) if err != nil { - logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime)) - return ctrl.Result{ - RequeueAfter: requeueTime, - }, nil + if requeueTime != 0 { + logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } else { + logger.Error(err, "unable to fetch CA certificate") + return ctrl.Result{}, err + } } logger.Info("Using custom CA certificate...") @@ -167,17 +184,27 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. r.SetReconcileStatusCondition(ctx, &infisicalPushSecretCRD, err) if err != nil { - logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime)) - return ctrl.Result{ - RequeueAfter: requeueTime, - }, nil + if requeueTime != 0 { + logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } else { + logger.Error(err, "unable to reconcile Infisical Push Secret") + return ctrl.Result{}, err + } } // Sync again after the specified time - logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime)) - return ctrl.Result{ - RequeueAfter: requeueTime, - }, nil + if requeueTime != 0 { + logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } else { + logger.Info("Operator will reconcile on next spec change") + return ctrl.Result{}, nil + } } func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -228,27 +255,67 @@ func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error )). Watches( &source.Kind{Type: &corev1.Secret{}}, - handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request { - ctx := context.Background() - pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{} - if err := r.List(ctx, pushSecrets); err != nil { - return []reconcile.Request{} - } - - requests := []reconcile.Request{} - for _, pushSecret := range pushSecrets.Items { - if pushSecret.Spec.Push.Secret.SecretName == o.GetName() && - pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() { - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: pushSecret.GetName(), - Namespace: pushSecret.GetNamespace(), - }, - }) - } - } - return requests - }), + handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForSecret), + ). + Watches( + &source.Kind{Type: &secretsv1alpha1.ClusterGenerator{}}, + handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForClusterGenerator), ). Complete(r) } + +func (r *InfisicalPushSecretReconciler) findPushSecretsForClusterGenerator(o client.Object) []reconcile.Request { + ctx := context.Background() + pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{} + if err := r.List(ctx, pushSecrets); err != nil { + return []reconcile.Request{} + } + + clusterGenerator, ok := o.(*secretsv1alpha1.ClusterGenerator) + if !ok { + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for _, pushSecret := range pushSecrets.Items { + if pushSecret.Spec.Push.Generators != nil { + for _, generator := range pushSecret.Spec.Push.Generators { + if generator.GeneratorRef.Name == clusterGenerator.GetName() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: pushSecret.GetName(), + Namespace: pushSecret.GetNamespace(), + }, + }) + break + } + } + } + } + return requests +} + +func (r *InfisicalPushSecretReconciler) findPushSecretsForSecret(o client.Object) []reconcile.Request { + ctx := context.Background() + pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{} + if err := r.List(ctx, pushSecrets); err != nil { + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for _, pushSecret := range pushSecrets.Items { + if pushSecret.Spec.Push.Secret != nil && + pushSecret.Spec.Push.Secret.SecretName == o.GetName() && + pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: pushSecret.GetName(), + Namespace: pushSecret.GetNamespace(), + }, + }) + } + + } + + return requests +} diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index 848df3c82..1400b163b 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -19,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + generatorUtil "github.com/Infisical/infisical/k8-operator/packages/generator" infisicalSdk "github.com/infisical/go-sdk" k8Errors "k8s.io/apimachinery/pkg/api/errors" ) @@ -106,6 +107,52 @@ func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSec infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables } +func (r *InfisicalPushSecretReconciler) processGenerators(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (map[string]string, error) { + + processedSecrets := make(map[string]string) + + if len(infisicalPushSecret.Spec.Push.Generators) == 0 { + return processedSecrets, nil + } + + for _, generator := range infisicalPushSecret.Spec.Push.Generators { + generatorRef := generator.GeneratorRef + + clusterGenerator := &v1alpha1.ClusterGenerator{} + err := r.Client.Get(ctx, types.NamespacedName{Name: generatorRef.Name}, clusterGenerator) + if err != nil { + return nil, fmt.Errorf("unable to get ClusterGenerator resource [err=%s]", err) + } + if generatorRef.Kind == v1alpha1.GeneratorKindPassword { + // get the custom ClusterGenerator resource from the cluster + + if clusterGenerator.Spec.Generator.PasswordSpec == nil { + return nil, fmt.Errorf("password spec is not defined in the ClusterGenerator resource") + } + + password, err := generatorUtil.GeneratorPassword(*clusterGenerator.Spec.Generator.PasswordSpec) + if err != nil { + return nil, fmt.Errorf("unable to generate password [err=%s]", err) + } + + processedSecrets[generator.DestinationSecretName] = password + } + + if generatorRef.Kind == v1alpha1.GeneratorKindUUID { + + uuid, err := generatorUtil.GeneratorUUID() + if err != nil { + return nil, fmt.Errorf("unable to generate UUID [err=%s]", err) + } + + processedSecrets[generator.DestinationSecretName] = uuid + } + } + + return processedSecrets, nil + +} + func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) { processedSecrets := make(map[string]string) @@ -172,18 +219,31 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context }) } - kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ - Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace, - Name: infisicalPushSecret.Spec.Push.Secret.SecretName, - }) + processedSecrets := make(map[string]string) - if err != nil { - return fmt.Errorf("unable to fetch kube secret [err=%s]", err) + if infisicalPushSecret.Spec.Push.Secret != nil { + kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ + Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace, + Name: infisicalPushSecret.Spec.Push.Secret.SecretName, + }) + + if err != nil { + return fmt.Errorf("unable to fetch kube secret [err=%s]", err) + } + + processedSecrets, err = r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination) + if err != nil { + return fmt.Errorf("unable to process templated secrets [err=%s]", err) + } } - processedSecrets, err := r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination) + generatorSecrets, err := r.processGenerators(ctx, infisicalPushSecret) if err != nil { - return fmt.Errorf("unable to process templated secrets [err=%s]", err) + return fmt.Errorf("unable to process generators [err=%s]", err) + } + + for key, value := range generatorSecrets { + processedSecrets[key] = value } destination := infisicalPushSecret.Spec.Destination diff --git a/k8-operator/go.mod b/k8-operator/go.mod index 6ce68ed7f..c9b868b00 100644 --- a/k8-operator/go.mod +++ b/k8-operator/go.mod @@ -4,10 +4,12 @@ go 1.21 require ( github.com/Masterminds/sprig/v3 v3.3.0 + github.com/aws/smithy-go v1.20.3 github.com/infisical/go-sdk v0.4.4 github.com/lestrrat-go/jwx/v2 v2.1.4 github.com/onsi/ginkgo/v2 v2.6.0 github.com/onsi/gomega v1.24.1 + github.com/sethvargo/go-password v0.3.1 k8s.io/apimachinery v0.26.1 k8s.io/client-go v0.26.1 sigs.k8s.io/controller-runtime v0.14.4 @@ -34,7 +36,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect - github.com/aws/smithy-go v1.20.3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -85,7 +86,7 @@ require ( github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/uuid v1.6.0 github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/k8-operator/go.sum b/k8-operator/go.sum index bcecfadc0..2e151b0a4 100644 --- a/k8-operator/go.sum +++ b/k8-operator/go.sum @@ -338,6 +338,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= +github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/k8-operator/packages/generator/generator.go b/k8-operator/packages/generator/generator.go new file mode 100644 index 000000000..cc1b290c7 --- /dev/null +++ b/k8-operator/packages/generator/generator.go @@ -0,0 +1 @@ +package generator diff --git a/k8-operator/packages/generator/password.go b/k8-operator/packages/generator/password.go new file mode 100644 index 000000000..d322f1014 --- /dev/null +++ b/k8-operator/packages/generator/password.go @@ -0,0 +1,76 @@ +package generator + +import ( + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/sethvargo/go-password/password" +) + +const ( + defaultLength = 24 + defaultSymbolChars = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./" + digitFactor = 0.25 + symbolFactor = 0.25 +) + +func generateSafePassword( + passLen int, + symbols int, + symbolCharacters string, + digits int, + noUpper bool, + allowRepeat bool, +) (string, error) { + gen, err := password.NewGenerator(&password.GeneratorInput{ + Symbols: symbolCharacters, + }) + if err != nil { + return "", err + } + return gen.Generate( + passLen, + digits, + symbols, + noUpper, + allowRepeat, + ) +} + +func GeneratorPassword(spec v1alpha1.PasswordSpec) (string, error) { + + symbolCharacters := defaultSymbolChars + + if spec.SymbolCharacters != nil && *spec.SymbolCharacters != "" { + symbolCharacters = *spec.SymbolCharacters + } + + passwordLength := defaultLength + + if spec.Length != 0 { + passwordLength = spec.Length + } + + digits := int(float32(passwordLength) * digitFactor) + if spec.Digits != nil { + digits = *spec.Digits + } + + symbols := int(float32(passwordLength) * symbolFactor) + if spec.Symbols != nil { + symbols = *spec.Symbols + } + + pass, err := generateSafePassword( + passwordLength, + symbols, + symbolCharacters, + digits, + spec.NoUpper, + spec.AllowRepeat, + ) + + if err != nil { + return "", err + } + + return pass, nil +} diff --git a/k8-operator/packages/generator/uuid.go b/k8-operator/packages/generator/uuid.go new file mode 100644 index 000000000..b9249f783 --- /dev/null +++ b/k8-operator/packages/generator/uuid.go @@ -0,0 +1,10 @@ +package generator + +import ( + "github.com/google/uuid" +) + +func GeneratorUUID() (string, error) { + uuid := uuid.New().String() + return uuid, nil +} diff --git a/k8-operator/packages/util/helpers.go b/k8-operator/packages/util/helpers.go index 02621dcfd..ef3712715 100644 --- a/k8-operator/packages/util/helpers.go +++ b/k8-operator/packages/util/helpers.go @@ -7,14 +7,19 @@ import ( "time" ) -func ConvertIntervalToDuration(resyncInterval string) (time.Duration, error) { - length := len(resyncInterval) +func ConvertIntervalToDuration(resyncInterval *string) (time.Duration, error) { + + if resyncInterval == nil || *resyncInterval == "" { + return 0, nil + } + + length := len(*resyncInterval) if length < 2 { return 0, fmt.Errorf("invalid format") } - unit := resyncInterval[length-1:] - numberPart := resyncInterval[:length-1] + unit := (*resyncInterval)[length-1:] + numberPart := (*resyncInterval)[:length-1] number, err := strconv.Atoi(numberPart) if err != nil { @@ -40,16 +45,6 @@ func ConvertIntervalToDuration(resyncInterval string) (time.Duration, error) { } } -func ConvertIntervalToTime(resyncInterval string) (time.Time, error) { - duration, err := ConvertIntervalToDuration(resyncInterval) - if err != nil { - return time.Time{}, err - } - - // Add duration to current time - return time.Now().Add(duration), nil -} - func AppendAPIEndpoint(address string) string { if strings.HasSuffix(address, "/api") { return address diff --git a/migration/.eslintrc.js b/migration/.eslintrc.js deleted file mode 100644 index cfc6baf3a..000000000 --- a/migration/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - "parserOptions": { - "ecmaVersion": 2017 - }, - "env": { - "es6": true - } - } \ No newline at end of file diff --git a/migration/README.md b/migration/README.md deleted file mode 100644 index 5d3984b23..000000000 --- a/migration/README.md +++ /dev/null @@ -1,3 +0,0 @@ -As Infisical's codebase matures, there are structural things that we need to change. - -This folder houses various migration scripts that can be used to upgrade self-hosted installations of Infisical to be compatible with newer versions. \ No newline at end of file diff --git a/migration/models/bot.js b/migration/models/bot.js deleted file mode 100644 index 85c93014c..000000000 --- a/migration/models/bot.js +++ /dev/null @@ -1,61 +0,0 @@ -var mongoose = require('mongoose'); - -var botSchema = new mongoose.Schema( - { - name: { - type: String, - required: true - }, - workspace: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - isActive: { - type: Boolean, - required: true, - default: false - }, - publicKey: { - type: String, - required: true - }, - encryptedPrivateKey: { - type: String, - required: true, - select: false - }, - iv: { - type: String, - required: true, - select: false - }, - tag: { - type: String, - required: true, - select: false - }, - algorithm: { // the encryption algorithm used - type: String, - enum: ['aes-256-gcm'], - required: true, - select: false - }, - keyEncoding: { - type: String, - enum: [ - 'utf8', - 'base64' - ], - required: true, - select: false - } - }, - { - timestamps: true - } -); - -var Bot = mongoose.model('Bot', botSchema); - -module.exports = Bot; diff --git a/migration/models/secretBlindIndexData.js b/migration/models/secretBlindIndexData.js deleted file mode 100644 index bcfa92394..000000000 --- a/migration/models/secretBlindIndexData.js +++ /dev/null @@ -1,43 +0,0 @@ -var mongoose = require('mongoose'); - -var secretBlindIndexDataSchema = new mongoose.Schema( - { - workspace: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - encryptedSaltCiphertext:{ - type: String, - required: true - }, - saltIV: { - type: String, - required: true - }, - saltTag: { - type: String, - required: true - }, - algorithm: { - type: String, - enum: ['aes-256-gcm'], - required: true, - select: false - }, - keyEncoding: { - type: String, - enum: [ - 'utf8', - 'base64' - ], - required: true, - select: false - } - - } -); - -var SecretBlindIndexData = mongoose.model('SecretBlindIndexData', secretBlindIndexDataSchema); - -module.exports = SecretBlindIndexData; \ No newline at end of file diff --git a/migration/models/user.js b/migration/models/user.js deleted file mode 100644 index 24e2c6554..000000000 --- a/migration/models/user.js +++ /dev/null @@ -1,83 +0,0 @@ -var mongoose = require('mongoose'); - -var userSchema = new mongoose.Schema( - { - email: { - type: String, - required: true - }, - firstName: { - type: String - }, - lastName: { - type: String - }, - encryptionVersion: { - type: Number, - select: false, - default: 1 // to resolve backward-compatibility issues - }, - protectedKey: { // introduced as part of encryption version 2 - type: String, - select: false - }, - protectedKeyIV: { // introduced as part of encryption version 2 - type: String, - select: false - }, - protectedKeyTag: { // introduced as part of encryption version 2 - type: String, - select: false - }, - publicKey: { - type: String, - select: false - }, - encryptedPrivateKey: { - type: String, - select: false - }, - iv: { // iv of [encryptedPrivateKey] - type: String, - select: false - }, - tag: { // tag of [encryptedPrivateKey] - type: String, - select: false - }, - salt: { - type: String, - select: false - }, - verifier: { - type: String, - select: false - }, - refreshVersion: { - type: Number, - default: 0, - select: false - }, - isMfaEnabled: { - type: Boolean, - default: false - }, - mfaMethods: [{ - type: String - }], - devices: { - type: [{ - ip: String, - userAgent: String - }], - default: [] - } - }, - { - timestamps: true - } -); - -var User = mongoose.model('User', userSchema); - -module.exports = User; diff --git a/migration/package-lock.json b/migration/package-lock.json deleted file mode 100644 index cf1084060..000000000 --- a/migration/package-lock.json +++ /dev/null @@ -1,444 +0,0 @@ -{ - "name": "migration", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "migration", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "dotenv": "^16.0.3", - "ip": "^2.0.1", - "mongoose": "^7.2.1" - } - }, - "node_modules/@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==" - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" - }, - "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "node_modules/bson": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.3.0.tgz", - "integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==", - "engines": { - "node": ">=14.20.1" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" - }, - "node_modules/kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, - "node_modules/mongodb": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.5.0.tgz", - "integrity": "sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ==", - "dependencies": { - "bson": "^5.3.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" - }, - "engines": { - "node": ">=14.20.1" - }, - "optionalDependencies": { - "saslprep": "^1.0.3" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.201.0", - "mongodb-client-encryption": ">=2.3.0 <3", - "snappy": "^7.2.2" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/mongoose": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.2.1.tgz", - "integrity": "sha512-c2OOl+ch9NlmPeJw7UjSb2jHNjoOw1XXHyzwygIf4z1GmaBx1OYb8OYqHkYPivvEmfY/vUWZFCgePsDqZgFn2w==", - "dependencies": { - "bson": "^5.3.0", - "kareem": "2.5.1", - "mongodb": "5.5.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "16.0.1" - }, - "engines": { - "node": ">=14.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/saslprep": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", - "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", - "optional": true, - "dependencies": { - "sparse-bitfield": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - } - }, - "dependencies": { - "@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==" - }, - "@types/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" - }, - "@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "requires": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "bson": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.3.0.tgz", - "integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" - }, - "ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" - }, - "kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==" - }, - "memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, - "mongodb": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.5.0.tgz", - "integrity": "sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ==", - "requires": { - "bson": "^5.3.0", - "mongodb-connection-string-url": "^2.6.0", - "saslprep": "^1.0.3", - "socks": "^2.7.1" - } - }, - "mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "requires": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "mongoose": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.2.1.tgz", - "integrity": "sha512-c2OOl+ch9NlmPeJw7UjSb2jHNjoOw1XXHyzwygIf4z1GmaBx1OYb8OYqHkYPivvEmfY/vUWZFCgePsDqZgFn2w==", - "requires": { - "bson": "^5.3.0", - "kareem": "2.5.1", - "mongodb": "5.5.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "16.0.1" - } - }, - "mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" - }, - "mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "requires": { - "debug": "4.x" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" - }, - "saslprep": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", - "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", - "optional": true, - "requires": { - "sparse-bitfield": "^3.0.3" - } - }, - "sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, - "socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "requires": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - } - }, - "sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, - "requires": { - "memory-pager": "^1.0.2" - } - }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - } - } -} diff --git a/migration/package.json b/migration/package.json deleted file mode 100644 index 9f431ca3a..000000000 --- a/migration/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "migration", - "version": "1.0.0", - "description": "As Infisical's codebase matures, there are structural things that we need to change.", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "dotenv": "^16.0.3", - "ip": "^2.0.1", - "mongoose": "^7.2.1" - } -} diff --git a/migration/scripts/reencrypt_under_256_key.js b/migration/scripts/reencrypt_under_256_key.js deleted file mode 100644 index 5f36bd2c7..000000000 --- a/migration/scripts/reencrypt_under_256_key.js +++ /dev/null @@ -1,177 +0,0 @@ -require('dotenv').config(); -const crypto = require('crypto'); -const mongoose = require('mongoose'); -const Bot = require('../models/bot'); -const SecretBlindIndexData = require('../models/secretBlindIndexData'); - -const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // 16-byte hex encryption key to migrate from -const ROOT_ENCRYPTION_KEY = process.env.ROOT_ENCRYPTION_KEY; // 32-byte base64 encryption key to migrate to - -const ALGORITHM_AES_256_GCM = 'aes-256-gcm'; -const ENCODING_SCHEME_UTF8 = 'utf8'; -const ENCODING_SCHEME_BASE64 = 'base64'; - -const decryptSymmetric = ({ - ciphertext, - iv, - tag, - key -}) => { - const decipher = crypto.createDecipheriv( - 'aes-256-gcm', - key, - Buffer.from(iv, ENCODING_SCHEME_BASE64) - ); - - decipher.setAuthTag(Buffer.from(tag, ENCODING_SCHEME_BASE64)); - - let cleartext = decipher.update(ciphertext, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const encryptSymmetric = ( - plaintext, - key -) => { - const iv = crypto.randomBytes(12); - - const secretKey = crypto.createSecretKey(key, ENCODING_SCHEME_BASE64); - const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, secretKey, iv); - - let ciphertext = cipher.update(plaintext, ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64); - ciphertext += cipher.final(ENCODING_SCHEME_BASE64); - - return { - ciphertext, - iv: iv.toString(ENCODING_SCHEME_BASE64), - tag: cipher.getAuthTag().toString(ENCODING_SCHEME_BASE64) - }; -}; - -/** - * Validate that encryption key [key] is encoded in [encoding] and [bytes] bytes - * @param {String} key - encryption key to validate - * @param {String} encoding - encoding like hex or base64 - * @param {Number} bytes - number of bytes - */ -const validateEncryptionKey = (encryptionKey, encoding, bytes) => { - const keyBuffer = Buffer.from(encryptionKey, encoding); - const decoded = keyBuffer.toString(encoding); - - if (decoded !== encryptionKey) throw Error({ - message: `Failed to validate that encryption key is encoded in ${encoding}` - }); - - if (keyBuffer.length !== bytes) throw Error({ - message: `Failed to validate that encryption key is ${bytes} bytes` - }); -} - -const main = async () => { - - // validate that ENCRYPTION_KEY is a 16-byte hex string - validateEncryptionKey(ENCRYPTION_KEY, 'hex', 16); - - // validate that ROOT_ENCRYPTION_KEY is a 32-byte base64 string - validateEncryptionKey(ROOT_ENCRYPTION_KEY, 'base64', 32); - - mongoose.connect(process.env.MONGO_URI) - .then(async () => { - console.log('Connected!'); - - if (ENCRYPTION_KEY && ROOT_ENCRYPTION_KEY) { - - // re-encrypt bot private keys - const bots = await Bot.find({ - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }).select('+encryptedPrivateKey iv tag algorithm keyEncoding workspace'); - - if (bots.length > 0) { - const operationsBot = await Promise.all( - bots.map(async (bot) => { - - const privateKey = decryptSymmetric({ - ciphertext: bot.encryptedPrivateKey, - iv: bot.iv, - tag: bot.tag, - key: ENCRYPTION_KEY - }); - - const { - ciphertext: encryptedPrivateKey, - iv, - tag - } = encryptSymmetric(privateKey, ROOT_ENCRYPTION_KEY); - - return ({ - updateOne: { - filter: { - _id: bot._id - }, - update: { - encryptedPrivateKey, - iv, - tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - } - } - }) - }) - ); - - const botBulkWriteResult = await Bot.bulkWrite(operationsBot); - console.log('botBulkWriteResult: ', botBulkWriteResult); - } - - // re-encrypt secret blind index data salts - const secretBlindIndexData = await SecretBlindIndexData.find({ - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }).select('+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding'); - - if (secretBlindIndexData.length > 0) { - const operationsSecretBlindIndexData = await Promise.all( - secretBlindIndexData.map(async (secretBlindIndexDatum) => { - - const salt = decryptSymmetric({ - ciphertext: secretBlindIndexDatum.encryptedSaltCiphertext, - iv: secretBlindIndexDatum.saltIV, - tag: secretBlindIndexDatum.saltTag, - key: ENCRYPTION_KEY - }); - - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = encryptSymmetric(salt, ROOT_ENCRYPTION_KEY); - - return ({ - updateOne: { - filter: { - _id: secretBlindIndexDatum._id - }, - update: { - encryptedSaltCiphertext, - saltIV, - saltTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - } - } - }) - }) - ); - - const secretBlindIndexDataBulkWriteResult = await SecretBlindIndexData.bulkWrite(operationsSecretBlindIndexData); - console.log('secretBlindIndexDataBulkWriteResult: ', secretBlindIndexDataBulkWriteResult); - } - } - }); -} - -main(); \ No newline at end of file