diff --git a/README.md b/README.md index f1393495d..c5017f8fa 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,8 @@ Not sure where to get started? You can: - Join our Slack, and ask us any questions there. -## Resources +## We are hiring! -- [Docs](https://infisical.com/docs/documentation/getting-started/introduction) for comprehensive documentation and guides -- [Slack](https://infisical.com/slack) for discussion with the community and Infisical team. -- [GitHub](https://github.com/Infisical/infisical) for code, issues, and pull requests -- [Twitter](https://twitter.com/infisical) for fast news -- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management -- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates +If you're reading this, there is a strong chance you like the products we created. + +You might also make a great addition to our team. We're growing fast and would love for you to [join us](https://infisical.com/careers). diff --git a/backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts b/backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts new file mode 100644 index 000000000..531cdcae8 --- /dev/null +++ b/backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.AppConnection, "gatewayId"))) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.uuid("gatewayId").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.AppConnection, "gatewayId")) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.dropColumn("gatewayId"); + }); + } +} diff --git a/backend/src/db/migrations/20250718133527_project-unify-revert.ts b/backend/src/db/migrations/20250718133527_project-unify-revert.ts new file mode 100644 index 000000000..53f1be9e4 --- /dev/null +++ b/backend/src/db/migrations/20250718133527_project-unify-revert.ts @@ -0,0 +1,433 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; +import { v4 as uuidV4 } from "uuid"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { ProjectType, TableName } from "../schemas"; + +/* eslint-disable no-await-in-loop,@typescript-eslint/ban-ts-comment */ + +// Single query to get all projects that need any kind of kickout +const getProjectsNeedingKickouts = async ( + knex: Knex +): Promise< + Array<{ + id: string; + defaultProduct: string; + needsSecretManager: boolean; + needsCertManager: boolean; + needsSecretScanning: boolean; + needsKms: boolean; + needsSsh: boolean; + }> +> => { + const result = await knex.raw( + ` +SELECT DISTINCT + p.id, + p."defaultProduct", + + -- Use CASE with direct joins instead of EXISTS subqueries + CASE WHEN p."defaultProduct" != 'secret-manager' AND s.secret_exists IS NOT NULL THEN true ELSE false END AS "needsSecretManager", + CASE WHEN p."defaultProduct" != 'cert-manager' AND ca.ca_exists IS NOT NULL THEN true ELSE false END AS "needsCertManager", + CASE WHEN p."defaultProduct" != 'secret-scanning' AND ssds.ssds_exists IS NOT NULL THEN true ELSE false END AS "needsSecretScanning", + CASE WHEN p."defaultProduct" != 'kms' AND kk.kms_exists IS NOT NULL THEN true ELSE false END AS "needsKms", + CASE WHEN p."defaultProduct" != 'ssh' AND sc.ssh_exists IS NOT NULL THEN true ELSE false END AS "needsSsh" + +FROM projects p +LEFT JOIN ( + SELECT DISTINCT e."projectId", 1 as secret_exists + FROM secrets_v2 s + JOIN secret_folders sf ON sf.id = s."folderId" + JOIN project_environments e ON e.id = sf."envId" +) s ON s."projectId" = p.id AND p."defaultProduct" != 'secret-manager' + +LEFT JOIN ( + SELECT DISTINCT "projectId", 1 as ca_exists + FROM certificate_authorities +) ca ON ca."projectId" = p.id AND p."defaultProduct" != 'cert-manager' + +LEFT JOIN ( + SELECT DISTINCT "projectId", 1 as ssds_exists + FROM secret_scanning_data_sources +) ssds ON ssds."projectId" = p.id AND p."defaultProduct" != 'secret-scanning' + +LEFT JOIN ( + SELECT DISTINCT "projectId", 1 as kms_exists + FROM kms_keys + WHERE "isReserved" = false +) kk ON kk."projectId" = p.id AND p."defaultProduct" != 'kms' + +LEFT JOIN ( + SELECT DISTINCT sca."projectId", 1 as ssh_exists + FROM ssh_certificates sc + JOIN ssh_certificate_authorities sca ON sca.id = sc."sshCaId" +) sc ON sc."projectId" = p.id AND p."defaultProduct" != 'ssh' + +WHERE p."defaultProduct" IS NOT NULL + AND ( + (p."defaultProduct" != 'secret-manager' AND s.secret_exists IS NOT NULL) OR + (p."defaultProduct" != 'cert-manager' AND ca.ca_exists IS NOT NULL) OR + (p."defaultProduct" != 'secret-scanning' AND ssds.ssds_exists IS NOT NULL) OR + (p."defaultProduct" != 'kms' AND kk.kms_exists IS NOT NULL) OR + (p."defaultProduct" != 'ssh' AND sc.ssh_exists IS NOT NULL) + ) + ` + ); + + return result.rows; +}; + +const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => { + const newProjectId = uuidV4(); + const project = await knex(TableName.Project).where("id", projectId).first(); + await knex(TableName.Project).insert({ + ...project, + type: projectType, + defaultProduct: null, + // @ts-ignore id is required + id: newProjectId, + slug: slugify(`${project?.name}-${alphaNumericNanoId(8)}`) + }); + + const customRoleMapping: Record = {}; + const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId); + if (projectCustomRoles.length) { + await knex.batchInsert( + TableName.ProjectRoles, + projectCustomRoles.map((el) => { + const id = uuidV4(); + customRoleMapping[el.id] = id; + return { + ...el, + id, + projectId: newProjectId, + permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions + }; + }) + ); + } + const groupMembershipMapping: Record = {}; + const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId); + if (groupMemberships.length) { + await knex.batchInsert( + TableName.GroupProjectMembership, + groupMemberships.map((el) => { + const id = uuidV4(); + groupMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn( + "projectMembershipId", + groupMemberships.map((el) => el.id) + ); + if (groupMembershipRoles.length) { + await knex.batchInsert( + TableName.GroupProjectMembershipRole, + groupMembershipRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = groupMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const identityProjectMembershipMapping: Record = {}; + const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId); + if (identities.length) { + await knex.batchInsert( + TableName.IdentityProjectMembership, + identities.map((el) => { + const id = uuidV4(); + identityProjectMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn( + "projectMembershipId", + identities.map((el) => el.id) + ); + if (identitiesRoles.length) { + await knex.batchInsert( + TableName.IdentityProjectMembershipRole, + identitiesRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const projectMembershipMapping: Record = {}; + const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId); + if (projectUserMembers.length) { + await knex.batchInsert( + TableName.ProjectMembership, + projectUserMembers.map((el) => { + const id = uuidV4(); + projectMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn( + "projectMembershipId", + projectUserMembers.map((el) => el.id) + ); + if (membershipRoles.length) { + await knex.batchInsert( + TableName.ProjectUserMembershipRole, + membershipRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = projectMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true); + if (kmsKeys.length) { + await knex.batchInsert( + TableName.KmsKey, + kmsKeys.map((el) => { + const id = uuidV4(); + const slug = slugify(alphaNumericNanoId(8).toLowerCase()); + return { ...el, id, slug, projectId: newProjectId }; + }) + ); + } + + const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first(); + if (projectBot) { + const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId }; + await knex(TableName.ProjectBot).insert(newProjectBot); + } + + const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId); + if (projectKeys.length) { + await knex.batchInsert( + TableName.ProjectKeys, + projectKeys.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const projectGateways = await knex(TableName.ProjectGateway).where("projectId", projectId); + if (projectGateways.length) { + await knex.batchInsert( + TableName.ProjectGateway, + projectGateways.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const projectSlackConfigs = await knex(TableName.ProjectSlackConfigs).where("projectId", projectId); + if (projectSlackConfigs.length) { + await knex.batchInsert( + TableName.ProjectSlackConfigs, + projectSlackConfigs.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const projectMicrosoftTeamsConfigs = await knex(TableName.ProjectMicrosoftTeamsConfigs).where("projectId", projectId); + if (projectMicrosoftTeamsConfigs.length) { + await knex.batchInsert( + TableName.ProjectMicrosoftTeamsConfigs, + projectMicrosoftTeamsConfigs.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const trustedIps = await knex(TableName.TrustedIps).where("projectId", projectId); + if (trustedIps.length) { + await knex.batchInsert( + TableName.TrustedIps, + trustedIps.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + return newProjectId; +}; + +const kickOutSecretManagerProject = async (knex: Knex, oldProjectId: string) => { + const newProjectId = await newProject(knex, oldProjectId, ProjectType.SecretManager); + await knex(TableName.IntegrationAuth).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.Environment).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SecretBlindIndex).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SecretSync).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SecretTag).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SecretReminderRecipients).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.ServiceToken).where("projectId", oldProjectId).update("projectId", newProjectId); +}; + +const kickOutCertManagerProject = async (knex: Knex, oldProjectId: string) => { + const newProjectId = await newProject(knex, oldProjectId, ProjectType.CertificateManager); + await knex(TableName.CertificateAuthority).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.Certificate).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.PkiSubscriber).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.PkiCollection).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.PkiAlert).where("projectId", oldProjectId).update("projectId", newProjectId); +}; + +const kickOutSecretScanningProject = async (knex: Knex, oldProjectId: string) => { + const newProjectId = await newProject(knex, oldProjectId, ProjectType.SecretScanning); + await knex(TableName.SecretScanningConfig).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SecretScanningDataSource).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SecretScanningFinding).where("projectId", oldProjectId).update("projectId", newProjectId); +}; + +const kickOutKmsProject = async (knex: Knex, oldProjectId: string) => { + const newProjectId = await newProject(knex, oldProjectId, ProjectType.KMS); + await knex(TableName.KmsKey) + .where("projectId", oldProjectId) + .andWhere("isReserved", false) + .update("projectId", newProjectId); + await knex(TableName.KmipClient).where("projectId", oldProjectId).update("projectId", newProjectId); +}; + +const kickOutSshProject = async (knex: Knex, oldProjectId: string) => { + const newProjectId = await newProject(knex, oldProjectId, ProjectType.SSH); + await knex(TableName.SshHost).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.ProjectSshConfig).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SshCertificateAuthority).where("projectId", oldProjectId).update("projectId", newProjectId); + await knex(TableName.SshHostGroup).where("projectId", oldProjectId).update("projectId", newProjectId); +}; + +const BATCH_SIZE = 1000; +const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes + +export async function up(knex: Knex): Promise { + const result = await knex.raw("SHOW statement_timeout"); + const originalTimeout = result.rows[0].statement_timeout; + + try { + await knex.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`); + + const hasTemplateTypeColumn = await knex.schema.hasColumn(TableName.ProjectTemplates, "type"); + if (hasTemplateTypeColumn) { + await knex(TableName.ProjectTemplates).whereNull("type").update({ + type: ProjectType.SecretManager + }); + await knex.schema.alterTable(TableName.ProjectTemplates, (t) => { + t.string("type").notNullable().defaultTo(ProjectType.SecretManager).alter(); + }); + } + + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); + const hasDefaultTypeColumn = await knex.schema.hasColumn(TableName.Project, "defaultProduct"); + if (hasTypeColumn && hasDefaultTypeColumn) { + await knex(TableName.Project).update({ + // eslint-disable-next-line + // @ts-ignore this is because this field is created later + type: knex.raw(`"defaultProduct"`) + }); + + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type").notNullable().alter(); + t.string("defaultProduct").nullable().alter(); + }); + + // Get all projects that need kickouts in a single query + const projectsNeedingKickouts = await getProjectsNeedingKickouts(knex); + + // Process projects in batches to avoid overwhelming the database + for (let i = 0; i < projectsNeedingKickouts.length; i += projectsNeedingKickouts.length) { + const batch = projectsNeedingKickouts.slice(i, i + BATCH_SIZE); + const processedIds: string[] = []; + + for (const project of batch) { + const kickoutPromises: Promise[] = []; + + // Only add kickouts that are actually needed (flags are pre-computed) + if (project.needsSecretManager) { + kickoutPromises.push(kickOutSecretManagerProject(knex, project.id)); + } + if (project.needsCertManager) { + kickoutPromises.push(kickOutCertManagerProject(knex, project.id)); + } + if (project.needsKms) { + kickoutPromises.push(kickOutKmsProject(knex, project.id)); + } + if (project.needsSsh) { + kickoutPromises.push(kickOutSshProject(knex, project.id)); + } + if (project.needsSecretScanning) { + kickoutPromises.push(kickOutSecretScanningProject(knex, project.id)); + } + + // Execute all kickouts in parallel and handle any failures gracefully + if (kickoutPromises.length > 0) { + const results = await Promise.allSettled(kickoutPromises); + + // Log any failures for debugging + results.forEach((res) => { + if (res.status === "rejected") { + throw new Error(`Migration failed for project ${project.id}: ${res.reason}`); + } + }); + } + + processedIds.push(project.id); + } + + // Clear defaultProduct for the processed batch + if (processedIds.length > 0) { + await knex(TableName.Project).whereIn("id", processedIds).update("defaultProduct", null); + } + } + } + } finally { + await knex.raw(`SET statement_timeout = '${originalTimeout}'`); + } +} + +export async function down(knex: Knex): Promise { + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); + const hasDefaultTypeColumn = await knex.schema.hasColumn(TableName.Project, "defaultProduct"); + if (hasTypeColumn && hasDefaultTypeColumn) { + await knex(TableName.Project).update({ + // eslint-disable-next-line + // @ts-ignore this is because this field is created later + defaultProduct: knex.raw(` + CASE + WHEN "type" IS NULL OR "type" = '' THEN 'secret-manager' + ELSE "type" + END + `) + }); + + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type").nullable().alter(); + t.string("defaultProduct").notNullable().alter(); + }); + } + + const hasTemplateTypeColumn = await knex.schema.hasColumn(TableName.ProjectTemplates, "type"); + if (hasTemplateTypeColumn) { + await knex.schema.alterTable(TableName.ProjectTemplates, (t) => { + t.string("type").nullable().alter(); + }); + } +} + diff --git a/backend/src/db/migrations/20250721101756_bump-aws-arn-field-size.ts b/backend/src/db/migrations/20250721101756_bump-aws-arn-field-size.ts new file mode 100644 index 000000000..1814df472 --- /dev/null +++ b/backend/src/db/migrations/20250721101756_bump-aws-arn-field-size.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns"); + if (hasColumn) { + await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => { + t.string("allowedPrincipalArns", 4096).notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns"); + if (hasColumn) { + await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => { + t.string("allowedPrincipalArns", 2048).notNullable().alter(); + }); + } +} diff --git a/backend/src/db/schemas/app-connections.ts b/backend/src/db/schemas/app-connections.ts index ee4282b73..2218b75ce 100644 --- a/backend/src/db/schemas/app-connections.ts +++ b/backend/src/db/schemas/app-connections.ts @@ -20,7 +20,8 @@ export const AppConnectionsSchema = z.object({ orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - isPlatformManagedCredentials: z.boolean().default(false).nullable().optional() + isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(), + gatewayId: z.string().uuid().nullable().optional() }); export type TAppConnections = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 8d7b2e69d..2e71486bf 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -270,6 +270,16 @@ export enum ProjectType { SecretScanning = "secret-scanning" } +export enum ActionProjectType { + SecretManager = ProjectType.SecretManager, + CertificateManager = ProjectType.CertificateManager, + KMS = ProjectType.KMS, + SSH = ProjectType.SSH, + SecretScanning = ProjectType.SecretScanning, + // project operations that happen on all types + Any = "any" +} + export enum SortDirection { ASC = "asc", DESC = "desc" diff --git a/backend/src/db/schemas/project-templates.ts b/backend/src/db/schemas/project-templates.ts index d1fe29a80..f12386165 100644 --- a/backend/src/db/schemas/project-templates.ts +++ b/backend/src/db/schemas/project-templates.ts @@ -16,7 +16,7 @@ export const ProjectTemplatesSchema = z.object({ orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - type: z.string().nullable().optional() + type: z.string().default("secret-manager") }); export type TProjectTemplates = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 00401575e..059565a94 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -25,12 +25,12 @@ export const ProjectsSchema = z.object({ kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), description: z.string().nullable().optional(), - type: z.string().nullable().optional(), + type: z.string(), enforceCapitalization: z.boolean().default(false), hasDeleteProtection: z.boolean().default(false).nullable().optional(), secretSharing: z.boolean().default(true), showSnapshotsLegacy: z.boolean().default(false), - defaultProduct: z.string().default("secret-manager") + defaultProduct: z.string().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/ee/routes/v1/pit-router.ts b/backend/src/ee/routes/v1/pit-router.ts index f993e31d7..14a82bce4 100644 --- a/backend/src/ee/routes/v1/pit-router.ts +++ b/backend/src/ee/routes/v1/pit-router.ts @@ -3,11 +3,14 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { removeTrailingSlash } from "@app/lib/fn"; -import { readLimit } from "@app/server/config/rateLimiter"; +import { isValidFolderName } from "@app/lib/validator"; +import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; +import { SecretNameSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { booleanSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { commitChangesResponseSchema, resourceChangeSchema } from "@app/services/folder-commit/folder-commit-schemas"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; const commitHistoryItemSchema = z.object({ id: z.string(), @@ -413,4 +416,166 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { return result; } }); + + server.route({ + method: "POST", + url: "/batch/commit", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: true, + description: "Commit changes", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + message: z + .string() + .trim() + .min(1) + .max(255) + .refine((message) => message.trim() !== "", { + message: "Commit message cannot be empty" + }), + changes: z.object({ + secrets: z.object({ + create: z + .array( + z.object({ + secretKey: SecretNameSchema, + secretValue: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), + secretComment: z.string().trim().optional().default(""), + skipMultilineEncoding: z.boolean().optional(), + metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tagIds: z.string().array().optional() + }) + ) + .optional(), + update: z + .array( + z.object({ + secretKey: SecretNameSchema, + newSecretName: SecretNameSchema.optional(), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional(), + secretComment: z.string().trim().optional().default(""), + skipMultilineEncoding: z.boolean().optional(), + metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tagIds: z.string().array().optional() + }) + ) + .optional(), + delete: z + .array( + z.object({ + secretKey: SecretNameSchema + }) + ) + .optional() + }), + folders: z.object({ + create: z + .array( + z.object({ + folderName: z + .string() + .trim() + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), + description: z.string().optional() + }) + ) + .optional(), + update: z + .array( + z.object({ + folderName: z + .string() + .trim() + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), + description: z.string().nullable().optional(), + id: z.string() + }) + ) + .optional(), + delete: z + .array( + z.object({ + folderName: z + .string() + .trim() + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), + id: z.string() + }) + ) + .optional() + }) + }) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const result = await server.services.pit.processNewCommitRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + message: req.body.message, + changes: { + secrets: req.body.changes.secrets, + folders: req.body.changes.folders + } + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.PIT_PROCESS_NEW_COMMIT_RAW, + metadata: { + commitId: result.commitId, + approvalId: result.approvalId, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + message: req.body.message + } + } + }); + + for await (const event of result.secretMutationEvents) { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.body.projectId, + event + }); + } + + return { message: "success" }; + } + }); }; diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index a00f4aa0b..c157b628b 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -1,6 +1,6 @@ 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 { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; @@ -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,10 @@ 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 projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg( + req.permission, + req.query.type + ); const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name)); @@ -191,6 +197,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), + type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type), environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional() }), response: { diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 5d4ccc021..5f8dea5d7 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -6,6 +6,7 @@ import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-r import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router"; +import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router"; import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; @@ -22,5 +23,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter, [SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter, [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, - [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter + [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter, + [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/okta-client-secret-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/okta-client-secret-rotation-router.ts new file mode 100644 index 000000000..133a70457 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/okta-client-secret-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateOktaClientSecretRotationSchema, + OktaClientSecretRotationGeneratedCredentialsSchema, + OktaClientSecretRotationSchema, + UpdateOktaClientSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/okta-client-secret"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerOktaClientSecretRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.OktaClientSecret, + server, + responseSchema: OktaClientSecretRotationSchema, + createSchema: CreateOktaClientSecretRotationSchema, + updateSchema: UpdateOktaClientSecretRotationSchema, + generatedCredentialsSchema: OktaClientSecretRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index 86768c3ad..7db99c8c4 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; +import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; @@ -23,7 +24,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ Auth0ClientSecretRotationListItemSchema, AzureClientSecretRotationListItemSchema, AwsIamUserSecretRotationListItemSchema, - LdapPasswordRotationListItemSchema + LdapPasswordRotationListItemSchema, + OktaClientSecretRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index fa487d0b7..6d656bafa 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -116,7 +117,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -272,7 +274,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); @@ -337,7 +340,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: accessApprovalPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -533,7 +537,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: policy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -583,7 +588,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); @@ -622,7 +628,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: policy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 8b823ee91..bdf579616 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import msFn from "ms"; -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -107,7 +107,8 @@ export const accessApprovalRequestServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); @@ -216,7 +217,7 @@ export const accessApprovalRequestServiceFactory = ({ ); const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; - const approvalUrl = `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`; + const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`; await triggerWorkflowIntegrationNotification({ input: { @@ -289,7 +290,8 @@ export const accessApprovalRequestServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); @@ -335,7 +337,8 @@ export const accessApprovalRequestServiceFactory = ({ actorId, projectId: accessApprovalRequest.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!membership) { @@ -551,7 +554,7 @@ export const accessApprovalRequestServiceFactory = ({ bypassReason: bypassReason || "No reason provided", secretPath: policy.secretPath || "/", environment, - approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`, + approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`, requestType: "access" }, template: SmtpTemplates.AccessSecretRequestBypassed @@ -582,7 +585,8 @@ export const accessApprovalRequestServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); diff --git a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts index f93abae83..38e0fc828 100644 --- a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts +++ b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts @@ -45,7 +45,10 @@ export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion( ]); export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdateOracleDBConnectionSchema = z @@ -54,7 +57,12 @@ export const UpdateOracleDBConnectionSchema = z AppConnections.UPDATE(AppConnection.OracleDB).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const OracleDBConnectionListItemSchema = z.object({ name: z.literal("OracleDB"), diff --git a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts index a63b0e3be..0c3d28364 100644 --- a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts +++ b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -37,7 +38,8 @@ export const assumePrivilegeServiceFactory = ({ actorId: actorPermissionDetails.id, projectId, actorAuthMethod: actorPermissionDetails.authMethod, - actorOrgId: actorPermissionDetails.orgId + actorOrgId: actorPermissionDetails.orgId, + actionProjectType: ActionProjectType.Any }); if (targetActorType === ActorType.USER) { @@ -58,7 +60,8 @@ export const assumePrivilegeServiceFactory = ({ actorId: targetActorId, projectId, actorAuthMethod: actorPermissionDetails.authMethod, - actorOrgId: actorPermissionDetails.orgId + actorOrgId: actorPermissionDetails.orgId, + actionProjectType: ActionProjectType.Any }); const appCfg = getConfig(); diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 333847734..06186d54b 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; +import { ActionProjectType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; @@ -37,7 +38,8 @@ export const auditLogServiceFactory = ({ actorId, projectId: filter.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); } else { diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 7ae3c267c..818bb1f99 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -449,6 +449,7 @@ export enum EventType { PIT_REVERT_COMMIT = "pit-revert-commit", PIT_GET_FOLDER_STATE = "pit-get-folder-state", PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states", + PIT_PROCESS_NEW_COMMIT_RAW = "pit-process-new-commit-raw", SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list", SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create", SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update", @@ -1550,8 +1551,9 @@ interface UpdateFolderEvent { metadata: { environment: string; folderId: string; - oldFolderName: string; + oldFolderName?: string; newFolderName: string; + newFolderDescription?: string; folderPath: string; }; } @@ -3226,6 +3228,18 @@ interface PitCompareFolderStatesEvent { }; } +interface PitProcessNewCommitRawEvent { + type: EventType.PIT_PROCESS_NEW_COMMIT_RAW; + metadata: { + projectId: string; + environment: string; + secretPath: string; + message: string; + approvalId?: string; + commitId?: string; + }; +} + interface SecretScanningDataSourceListEvent { type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST; metadata: { @@ -3687,6 +3701,7 @@ export type Event = | PitRevertCommitEvent | PitCompareFolderStatesEvent | PitGetFolderStateEvent + | PitProcessNewCommitRawEvent | SecretScanningDataSourceListEvent | SecretScanningDataSourceGetEvent | SecretScanningDataSourceCreateEvent diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index cc6a6b5fe..5ead798fa 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -77,7 +78,8 @@ export const certificateAuthorityCrlServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index c2c596922..cf37626c7 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import RE2 from "re2"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -84,7 +85,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const plan = await licenseService.getPlan(actorOrgId); @@ -200,7 +202,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ @@ -297,7 +300,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ @@ -385,7 +389,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -432,7 +437,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index ab59b1a1d..73dcbe6e3 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -78,7 +79,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -207,7 +209,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const plan = await licenseService.getPlan(actorOrgId); @@ -358,7 +361,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -423,7 +427,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -487,7 +492,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); // verify user has access to each env in request @@ -530,7 +536,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -578,7 +585,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -615,7 +623,8 @@ export const dynamicSecretServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager }); const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) => @@ -659,7 +668,8 @@ export const dynamicSecretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 485e46885..64da588f8 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import { TableName } from "@app/db/schemas"; +import { ActionProjectType, TableName } from "@app/db/schemas"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -61,7 +61,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, @@ -72,7 +73,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId: identityId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // we need to validate that the privilege given is not higher than the assigning users permission @@ -158,7 +160,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, @@ -169,7 +172,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId: identityProjectMembership.identityId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // we need to validate that the privilege given is not higher than the assigning users permission @@ -256,7 +260,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, @@ -267,7 +272,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId: identityProjectMembership.identityId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, @@ -315,7 +321,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, @@ -349,7 +356,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, @@ -384,7 +392,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 747e13f15..828cf43a3 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; +import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -72,7 +73,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( @@ -85,7 +87,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId: identityId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // we need to validate that the privilege given is not higher than the assigning users permission @@ -172,7 +175,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( @@ -185,7 +189,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId: identityProjectMembership.identityId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // we need to validate that the privilege given is not higher than the assigning users permission @@ -288,7 +293,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, @@ -300,7 +306,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId: identityProjectMembership.identityId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, @@ -359,7 +366,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, @@ -401,7 +409,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorId, projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 992b31017..8daa5a37a 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ActionProjectType } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { isValidIp } from "@app/lib/ip"; @@ -78,7 +79,8 @@ export const kmipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan( @@ -131,7 +133,8 @@ export const kmipServiceFactory = ({ actorId, projectId: kmipClient.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan( @@ -162,7 +165,8 @@ export const kmipServiceFactory = ({ actorId, projectId: kmipClient.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan( @@ -195,7 +199,8 @@ export const kmipServiceFactory = ({ actorId, projectId: kmipClient.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); @@ -216,7 +221,8 @@ export const kmipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); @@ -252,7 +258,8 @@ export const kmipServiceFactory = ({ actorId, projectId: kmipClient.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 72df88982..5e71c65d9 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -1,6 +1,7 @@ import { MongoAbility, RawRuleOf } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; +import { ActionProjectType } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionSet } from "./org-permission"; @@ -20,6 +21,7 @@ export type TGetUserProjectPermissionArg = { userId: string; projectId: string; authMethod: ActorAuthMethod; + actionProjectType: ActionProjectType; userOrgId?: string; }; @@ -27,12 +29,14 @@ export type TGetIdentityProjectPermissionArg = { identityId: string; projectId: string; identityOrgId?: string; + actionProjectType: ActionProjectType; }; export type TGetServiceTokenProjectPermissionArg = { serviceTokenId: string; projectId: string; actorOrgId?: string; + actionProjectType: ActionProjectType; }; export type TGetProjectPermissionArg = { @@ -41,6 +45,7 @@ export type TGetProjectPermissionArg = { projectId: string; actorAuthMethod: ActorAuthMethod; actorOrgId?: string; + actionProjectType: ActionProjectType; }; export type TPermissionServiceFactory = { @@ -138,7 +143,13 @@ export type TPermissionServiceFactory = { }; } >; - getUserProjectPermission: ({ userId, projectId, authMethod, userOrgId }: TGetUserProjectPermissionArg) => Promise<{ + getUserProjectPermission: ({ + userId, + projectId, + authMethod, + userOrgId, + actionProjectType + }: TGetUserProjectPermissionArg) => Promise<{ permission: MongoAbility; membership: { id: string; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 32c01dcfb..85ee82cca 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -5,6 +5,7 @@ import { MongoQuery } from "@ucast/mongo2js"; import handlebars from "handlebars"; import { + ActionProjectType, OrgMembershipRole, ProjectMembershipRole, ServiceTokenScopes, @@ -213,7 +214,8 @@ export const permissionServiceFactory = ({ userId, projectId, authMethod, - userOrgId + userOrgId, + actionProjectType }: TGetUserProjectPermissionArg): Promise> => { const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" }); @@ -240,6 +242,12 @@ export const permissionServiceFactory = ({ userProjectPermission.orgRole ); + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { + throw new BadRequestError({ + message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` + }); + } + // join two permissions and pass to build the final permission set const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const additionalPrivileges = @@ -287,7 +295,8 @@ export const permissionServiceFactory = ({ const getIdentityProjectPermission = async ({ identityId, projectId, - identityOrgId + identityOrgId, + actionProjectType }: TGetIdentityProjectPermissionArg): Promise> => { const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); if (!identityProjectPermission) @@ -307,6 +316,12 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" }); } + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) { + throw new BadRequestError({ + message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` + }); + } + const rolePermissions = identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const additionalPrivileges = @@ -361,7 +376,8 @@ export const permissionServiceFactory = ({ const getServiceTokenProjectPermission = async ({ serviceTokenId, projectId, - actorOrgId + actorOrgId, + actionProjectType }: TGetServiceTokenProjectPermissionArg) => { const serviceToken = await serviceTokenDAL.findById(serviceTokenId); if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` }); @@ -386,6 +402,12 @@ export const permissionServiceFactory = ({ }); } + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== serviceTokenProject.type) { + throw new BadRequestError({ + message: `The project is of type ${serviceTokenProject.type}. Operations of type ${actionProjectType} are not allowed.` + }); + } + const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), @@ -537,7 +559,8 @@ export const permissionServiceFactory = ({ actorId: inputActorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType }: TGetProjectPermissionArg): Promise> => { let actor = inputActor; let actorId = inputActorId; @@ -558,19 +581,22 @@ export const permissionServiceFactory = ({ userId: actorId, projectId, authMethod: actorAuthMethod, - userOrgId: actorOrgId + userOrgId: actorOrgId, + actionProjectType }) as Promise>; case ActorType.SERVICE: return getServiceTokenProjectPermission({ serviceTokenId: actorId, projectId, - actorOrgId + actorOrgId, + actionProjectType }) as Promise>; case ActorType.IDENTITY: return getIdentityProjectPermission({ identityId: actorId, projectId, - identityOrgId: actorOrgId + identityOrgId: actorOrgId, + actionProjectType }) as Promise>; default: throw new BadRequestError({ diff --git a/backend/src/ee/services/pit/pit-service.ts b/backend/src/ee/services/pit/pit-service.ts index c2a485b35..ef7f9b5a3 100644 --- a/backend/src/ee/services/pit/pit-service.ts +++ b/backend/src/ee/services/pit/pit-service.ts @@ -1,29 +1,53 @@ /* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; +import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; -import { ResourceType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service"; +import { TFolderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal"; +import { + ResourceType, + TCommitResourceChangeDTO, + TFolderCommitServiceFactory +} from "@app/services/folder-commit/folder-commit-service"; import { isFolderCommitChange, isSecretCommitChange } from "@app/services/folder-commit-changes/folder-commit-changes-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; +import { TProcessNewCommitRawDTO } from "@app/services/secret/secret-types"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; +import { TSecretV2BridgeServiceFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-service"; +import { SecretOperations, SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; +import { TSecretApprovalPolicyServiceFactory } from "../secret-approval-policy/secret-approval-policy-service"; +import { TSecretApprovalRequestServiceFactory } from "../secret-approval-request/secret-approval-request-service"; type TPitServiceFactoryDep = { folderCommitService: TFolderCommitServiceFactory; secretService: Pick; - folderService: Pick; + folderService: Pick< + TSecretFolderServiceFactory, + "getFolderById" | "getFolderVersions" | "createManyFolders" | "updateManyFolders" | "deleteManyFolders" + >; permissionService: Pick; - folderDAL: Pick; + folderDAL: Pick; projectEnvDAL: Pick; + secretApprovalRequestService: Pick< + TSecretApprovalRequestServiceFactory, + "generateSecretApprovalRequest" | "generateSecretApprovalRequestV2Bridge" + >; + secretApprovalPolicyService: Pick; + projectDAL: Pick; + secretV2BridgeService: TSecretV2BridgeServiceFactory; + folderCommitDAL: Pick; }; export type TPitServiceFactory = ReturnType; @@ -34,7 +58,12 @@ export const pitServiceFactory = ({ folderService, permissionService, folderDAL, - projectEnvDAL + projectEnvDAL, + secretApprovalRequestService, + secretApprovalPolicyService, + projectDAL, + secretV2BridgeService, + folderCommitDAL }: TPitServiceFactoryDep) => { const getCommitsCount = async ({ actor, @@ -320,7 +349,8 @@ export const pitServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(userPermission).throwUnlessCan( @@ -471,6 +501,347 @@ export const pitServiceFactory = ({ }); }; + const processNewCommitRaw = async ({ + actorId, + projectId, + environment, + actor, + actorOrgId, + actorAuthMethod, + secretPath, + message, + changes = { + secrets: { + create: [], + update: [], + delete: [] + }, + folders: { + create: [], + update: [], + delete: [] + } + } + }: { + actorId: string; + projectId: string; + environment: string; + actor: ActorType; + actorOrgId: string; + actorAuthMethod: ActorAuthMethod; + secretPath: string; + message: string; + changes: TProcessNewCommitRawDTO; + }) => { + const policy = + actor === ActorType.USER + ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) + : undefined; + const secretMutationEvents: Event[] = []; + + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + const caseViolatingSecretKeys = [ + // Check create operations + ...(changes.secrets?.create + ?.filter((sec) => sec.secretKey !== sec.secretKey.toUpperCase()) + .map((sec) => sec.secretKey) ?? []), + + // Check update operations + ...(changes.secrets?.update + ?.filter((sec) => sec.newSecretName && sec.newSecretName !== sec.newSecretName.toUpperCase()) + .map((sec) => sec.secretKey) ?? []) + ]; + + if (caseViolatingSecretKeys.length) { + throw new BadRequestError({ + message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join( + ", " + )}. You can disable this requirement in project settings` + }); + } + } + + const response = await folderCommitDAL.transaction(async (trx) => { + const targetFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, trx); + if (!targetFolder) + throw new NotFoundError({ + message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`, + name: "CreateManySecret" + }); + const commitChanges: TCommitResourceChangeDTO[] = []; + const folderChanges: { create: string[]; update: string[]; delete: string[] } = { + create: [], + update: [], + delete: [] + }; + + if ((changes.folders?.create?.length ?? 0) > 0) { + const createdFolders = await folderService.createManyFolders({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + folders: + changes.folders?.create?.map((folder) => ({ + name: folder.folderName, + environment, + path: secretPath, + description: folder.description + })) ?? [], + tx: trx, + commitChanges + }); + const newFolderEvents = createdFolders.folders.map( + (folder) => + ({ + type: EventType.CREATE_FOLDER, + metadata: { + environment, + folderId: folder.id, + folderName: folder.name, + folderPath: secretPath, + ...(folder.description ? { description: folder.description } : {}) + } + }) as Event + ); + secretMutationEvents.push(...newFolderEvents); + folderChanges.create.push(...createdFolders.folders.map((folder) => folder.id)); + } + + if ((changes.folders?.update?.length ?? 0) > 0) { + const updatedFolders = await folderService.updateManyFolders({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + folders: + changes.folders?.update?.map((folder) => ({ + environment, + path: secretPath, + id: folder.id, + name: folder.folderName, + description: folder.description + })) ?? [], + tx: trx, + commitChanges + }); + + const updatedFolderEvents = updatedFolders.newFolders.map( + (folder) => + ({ + type: EventType.UPDATE_FOLDER, + metadata: { + environment, + folderId: folder.id, + folderPath: secretPath, + newFolderName: folder.name, + newFolderDescription: folder.description + } + }) as Event + ); + secretMutationEvents.push(...updatedFolderEvents); + folderChanges.update.push(...updatedFolders.newFolders.map((folder) => folder.id)); + } + + if ((changes.folders?.delete?.length ?? 0) > 0) { + const deletedFolders = await folderService.deleteManyFolders({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + folders: + changes.folders?.delete?.map((folder) => ({ + environment, + path: secretPath, + idOrName: folder.id + })) ?? [], + tx: trx, + commitChanges + }); + const deletedFolderEvents = deletedFolders.folders.map( + (folder) => + ({ + type: EventType.DELETE_FOLDER, + metadata: { + environment, + folderId: folder.id, + folderPath: secretPath, + folderName: folder.name + } + }) as Event + ); + secretMutationEvents.push(...deletedFolderEvents); + folderChanges.delete.push(...deletedFolders.folders.map((folder) => folder.id)); + } + + if (policy) { + if ( + (changes.secrets?.create?.length ?? 0) > 0 || + (changes.secrets?.update?.length ?? 0) > 0 || + (changes.secrets?.delete?.length ?? 0) > 0 + ) { + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + policy, + secretPath, + environment, + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + data: { + [SecretOperations.Create]: + changes.secrets?.create?.map((el) => ({ + tagIds: el.tagIds, + secretValue: el.secretValue, + secretComment: el.secretComment, + metadata: el.metadata, + skipMultilineEncoding: el.skipMultilineEncoding, + secretKey: el.secretKey, + secretMetadata: el.secretMetadata + })) ?? [], + [SecretOperations.Update]: + changes.secrets?.update?.map((el) => ({ + tagIds: el.tagIds, + newSecretName: el.newSecretName, + secretValue: el.secretValue, + secretComment: el.secretComment, + metadata: el.metadata, + skipMultilineEncoding: el.skipMultilineEncoding, + secretKey: el.secretKey, + secretMetadata: el.secretMetadata + })) ?? [], + [SecretOperations.Delete]: + changes.secrets?.delete?.map((el) => ({ + secretKey: el.secretKey + })) ?? [] + } + }); + return { + approvalId: approval.id, + folderChanges, + secretMutationEvents + }; + } + return { + folderChanges, + secretMutationEvents + }; + } + + if ((changes.secrets?.create?.length ?? 0) > 0) { + const newSecrets = await secretV2BridgeService.createManySecret({ + secretPath, + environment, + projectId, + actorAuthMethod, + actorOrgId, + actor, + actorId, + secrets: changes.secrets?.create ?? [], + tx: trx, + commitChanges + }); + secretMutationEvents.push({ + type: EventType.CREATE_SECRETS, + metadata: { + environment, + secretPath, + secrets: newSecrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version + })) + } + }); + } + if ((changes.secrets?.update?.length ?? 0) > 0) { + const updatedSecrets = await secretV2BridgeService.updateManySecret({ + secretPath, + environment, + projectId, + actorAuthMethod, + actorOrgId, + actor, + actorId, + secrets: changes.secrets?.update ?? [], + mode: SecretUpdateMode.FailOnNotFound, + tx: trx, + commitChanges + }); + secretMutationEvents.push({ + type: EventType.UPDATE_SECRETS, + metadata: { + environment, + secretPath, + secrets: updatedSecrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version + })) + } + }); + } + if ((changes.secrets?.delete?.length ?? 0) > 0) { + const deletedSecrets = await secretV2BridgeService.deleteManySecret({ + secretPath, + environment, + projectId, + actorAuthMethod, + actorOrgId, + actor, + actorId, + secrets: changes.secrets?.delete ?? [], + tx: trx, + commitChanges + }); + secretMutationEvents.push({ + type: EventType.DELETE_SECRETS, + metadata: { + environment, + secretPath, + secrets: deletedSecrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version + })) + } + }); + } + if (commitChanges?.length > 0) { + const commit = await folderCommitService.createCommit( + { + actor: { + type: actor || ActorType.PLATFORM, + metadata: { + id: actorId + } + }, + message, + folderId: targetFolder.id, + changes: commitChanges + }, + trx + ); + return { + folderChanges, + commitId: commit?.id, + secretMutationEvents + }; + } + return { + folderChanges, + secretMutationEvents + }; + }); + + return response; + }; + return { getCommitsCount, getCommitsForFolder, @@ -478,6 +849,7 @@ export const pitServiceFactory = ({ compareCommitChanges, rollbackToCommit, revertCommit, - getFolderStateAtCommit + getFolderStateAtCommit, + processNewCommitRaw }; }; 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 5d4d0a953..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,3 +1,4 @@ +import { ProjectType } from "@app/db/schemas"; import { InfisicalProjectTemplate, TUnpackedPermission @@ -6,18 +7,21 @@ import { getPredefinedRoles } from "@app/services/project-role/project-role-fns" import { ProjectTemplateDefaultEnvironments } from "./project-template-constants"; -export const getDefaultProjectTemplate = (orgId: string) => ({ +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({ projectId: "project-template" }) as Array<{ - name: string; - slug: string; - permissions: 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 510105572..f3fe07aa8 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -1,7 +1,7 @@ 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-types"; @@ -29,11 +29,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla ...rest, environments: environments as TProjectTemplateEnvironment[], roles: [ - ...getPredefinedRoles({ projectId: "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) @@ -46,7 +48,10 @@ export const projectTemplateServiceFactory = ({ permissionService, projectTemplateDAL }: TProjectTemplatesServiceFactoryDep): TProjectTemplateServiceFactory => { - const listProjectTemplatesByOrg: TProjectTemplateServiceFactory["listProjectTemplatesByOrg"] = async (actor) => { + const listProjectTemplatesByOrg: TProjectTemplateServiceFactory["listProjectTemplatesByOrg"] = async ( + actor, + type + ) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) @@ -65,11 +70,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 +142,7 @@ export const projectTemplateServiceFactory = ({ }; const createProjectTemplate: TProjectTemplateServiceFactory["createProjectTemplate"] = async ( - { roles, environments, ...params }, + { roles, environments, type, ...params }, actor ) => { const plan = await licenseService.getPlan(actor.orgId); @@ -154,6 +162,10 @@ 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 @@ -176,8 +188,10 @@ export const projectTemplateServiceFactory = ({ const projectTemplate = await projectTemplateDAL.create({ ...params, roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))), - environments: environments ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null, - orgId: actor.orgId + environments: + type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null, + orgId: actor.orgId, + type }); return $unpackProjectTemplate(projectTemplate); @@ -208,6 +222,11 @@ 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({ 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 e705a096d..8d9e952a7 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 { ProjectMembershipRole, TProjectEnvironments } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { OrgServiceActor } from "@app/lib/types"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; @@ -15,6 +15,7 @@ export type TProjectTemplateRole = { export type TCreateProjectTemplateDTO = { name: string; + type: ProjectType; description?: string; roles: TProjectTemplateRole[]; environments?: TProjectTemplateEnvironment[] | null; @@ -29,11 +30,15 @@ export enum InfisicalProjectTemplate { } export type TProjectTemplateServiceFactory = { - listProjectTemplatesByOrg: (actor: OrgServiceActor) => Promise< + listProjectTemplatesByOrg: ( + actor: OrgServiceActor, + type?: ProjectType + ) => Promise< ( | { id: string; name: InfisicalProjectTemplate; + type: string; createdAt: Date; updatedAt: Date; description: string; @@ -58,6 +63,7 @@ export type TProjectTemplateServiceFactory = { } | { environments: TProjectTemplateEnvironment[]; + type: string; roles: { permissions: { action: string[]; @@ -94,6 +100,7 @@ export type TProjectTemplateServiceFactory = { }[]; name: string; orgId: string; + type: string; id: string; createdAt: Date; updatedAt: Date; @@ -118,6 +125,7 @@ export type TProjectTemplateServiceFactory = { name: string; orgId: string; id: string; + type: string; createdAt: Date; updatedAt: Date; description?: string | null | undefined; @@ -140,6 +148,7 @@ export type TProjectTemplateServiceFactory = { name: string; orgId: string; id: string; + type: string; createdAt: Date; updatedAt: Date; description?: string | null | undefined; @@ -162,6 +171,7 @@ export type TProjectTemplateServiceFactory = { }[]; name: string; orgId: string; + type: string; id: string; createdAt: Date; updatedAt: Date; @@ -184,6 +194,7 @@ export type TProjectTemplateServiceFactory = { name: string; }[]; name: string; + type: string; orgId: string; id: string; createdAt: Date; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index d44ab054d..944775156 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import { TableName } from "@app/db/schemas"; +import { ActionProjectType, TableName } from "@app/db/schemas"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -61,7 +61,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ @@ -69,7 +70,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId: projectMembership.userId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // we need to validate that the privilege given is not higher than the assigning users permission @@ -164,7 +166,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const { permission: targetUserPermission } = await permissionService.getProjectPermission({ @@ -172,7 +175,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId: projectMembership.userId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // we need to validate that the privilege given is not higher than the assigning users permission @@ -272,7 +276,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); @@ -317,7 +322,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); @@ -343,7 +349,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorId, projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 80127c071..41a635e2f 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import picomatch from "picomatch"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -110,7 +111,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -304,7 +306,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId: secretApprovalPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -459,7 +462,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId: sapPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -498,7 +502,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); @@ -542,7 +547,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); return getSecretApprovalPolicy(projectId, environment, secretPath); @@ -568,7 +574,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId: sapPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts index 5e4e0e3d6..dfe425b8e 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts @@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({ firstName: reviewerUser.firstName, projectName: project.name, organizationName: project.organization.name, - approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval?requestId=${secretApprovalRequest.id}` + approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}` }, template: SmtpTemplates.SecretApprovalRequestNeedsReview }); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 2a755f9a6..2be0361e0 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -1,7 +1,9 @@ /* eslint-disable no-nested-ternary */ import { ForbiddenError, subject } from "@casl/ability"; +import { Knex } from "knex"; import { + ActionProjectType, ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, @@ -183,7 +185,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId, policyId); @@ -210,7 +213,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -262,7 +266,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -411,7 +416,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId: secretApprovalRequest.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -480,7 +486,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId: secretApprovalRequest.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -536,7 +543,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( @@ -954,7 +962,7 @@ export const secretApprovalRequestServiceFactory = ({ bypassReason, secretPath: policy.secretPath, environment: env.name, - approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval` + approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval` }, template: SmtpTemplates.AccessSecretRequestBypassed }); @@ -1088,7 +1096,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { @@ -1368,8 +1377,9 @@ export const secretApprovalRequestServiceFactory = ({ policy, projectId, secretPath, - environment - }: TGenerateSecretApprovalRequestV2BridgeDTO) => { + environment, + trx: providedTx + }: TGenerateSecretApprovalRequestV2BridgeDTO & { trx?: Knex }) => { if (actor === ActorType.SERVICE || actor === ActorType.Machine) throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" }); @@ -1378,7 +1388,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1595,7 +1606,7 @@ export const secretApprovalRequestServiceFactory = ({ ); }); - const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => { + const executeApprovalRequestCreation = async (tx: Knex) => { const doc = await secretApprovalRequestDAL.create( { folderId, @@ -1657,7 +1668,11 @@ export const secretApprovalRequestServiceFactory = ({ } return { ...doc, commits: approvalCommits }; - }); + }; + + const secretApprovalRequest = providedTx + ? await executeApprovalRequestCreation(providedTx) + : await secretApprovalRequestDAL.transaction(executeApprovalRequestCreation); const user = await userDAL.findById(actorId); const env = await projectEnvDAL.findOne({ id: policy.envId }); diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/index.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/index.ts new file mode 100644 index 000000000..8a1026194 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/index.ts @@ -0,0 +1,3 @@ +export * from "./okta-client-secret-rotation-constants"; +export * from "./okta-client-secret-rotation-schemas"; +export * from "./okta-client-secret-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-constants.ts new file mode 100644 index 000000000..35347f6b4 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "Okta Client Secret", + type: SecretRotation.OktaClientSecret, + connection: AppConnection.Okta, + template: { + secretsMapping: { + clientId: "OKTA_CLIENT_ID", + clientSecret: "OKTA_CLIENT_SECRET" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-fns.ts new file mode 100644 index 000000000..0fe4438d1 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-fns.ts @@ -0,0 +1,273 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { request } from "@app/lib/config/request"; +import { delay as delayMs } from "@app/lib/delay"; +import { BadRequestError } from "@app/lib/errors"; +import { getOktaInstanceUrl } from "@app/services/app-connection/okta"; + +import { + TOktaClientSecret, + TOktaClientSecretRotationGeneratedCredentials, + TOktaClientSecretRotationWithConnection +} from "./okta-client-secret-rotation-types"; + +type OktaErrorResponse = { errorCode: string; errorSummary: string; errorCauses?: { errorSummary: string }[] }; + +const isOktaErrorResponse = (data: unknown): data is OktaErrorResponse => { + return ( + typeof data === "object" && + data !== null && + "errorSummary" in data && + typeof (data as OktaErrorResponse).errorSummary === "string" + ); +}; + +const createErrorMessage = (error: unknown) => { + if (error instanceof AxiosError) { + if (error.response?.data && isOktaErrorResponse(error.response.data)) { + const oktaError = error.response.data; + if (oktaError.errorCauses && oktaError.errorCauses.length > 0) { + return oktaError.errorCauses[0].errorSummary; + } + return oktaError.errorSummary; + } + if (error.message) { + return error.message; + } + } + return "Unknown error"; +}; + +// Delay between each revocation call in revokeCredentials +const DELAY_MS = 1000; + +export const oktaClientSecretRotationFactory: TRotationFactory< + TOktaClientSecretRotationWithConnection, + TOktaClientSecretRotationGeneratedCredentials +> = (secretRotation) => { + const { + connection, + parameters: { clientId }, + secretsMapping + } = secretRotation; + + /** + * Creates a new client secret for the Okta app. + */ + const $rotateClientSecret = async () => { + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + const { data } = await request.post( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets`, + {}, + { + headers: { + Accept: "application/json", + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + if (!data.client_secret || !data.id) { + throw new Error("Invalid response from Okta: missing 'client_secret' or secret 'id'."); + } + + return { + clientSecret: data.client_secret, + secretId: data.id, + clientId + }; + } catch (error: unknown) { + if ( + error instanceof AxiosError && + error.response?.data && + isOktaErrorResponse(error.response.data) && + error.response.data.errorCode === "E0000001" + ) { + // Okta has a maximum of 2 secrets per app, thus we must warn the users in case they already have 2 + throw new BadRequestError({ + message: `Failed to add client secret to Okta app ${clientId}: You must have only a single secret for the Okta app prior to creating this secret rotation.` + }); + } + + throw new BadRequestError({ + message: `Failed to add client secret to Okta app ${clientId}: ${createErrorMessage(error)}` + }); + } + }; + + /** + * List client secrets. + */ + const $listClientSecrets = async () => { + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + const { data } = await request.get( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets`, + { + headers: { + Accept: "application/json", + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + return data; + } catch (error: unknown) { + throw new BadRequestError({ + message: `Failed to list client secrets for Okta app ${clientId}: ${createErrorMessage(error)}` + }); + } + }; + + /** + * Checks if a credential with the given secretId exists. + */ + const credentialExists = async (secretId: string): Promise => { + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + const { data } = await request.get( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}`, + { + headers: { + Accept: "application/json", + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + return data.id === secretId; + } catch (_) { + return false; + } + }; + + /** + * Revokes a client secret from the Okta app using its secretId. + * First checks if the credential exists before attempting revocation. + */ + const revokeCredential = async (secretId: string) => { + // Check if credential exists before attempting revocation + const exists = await credentialExists(secretId); + if (!exists) { + return; // Credential doesn't exist, nothing to revoke + } + + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + // First deactivate the secret + await request.post( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}/lifecycle/deactivate`, + undefined, + { + headers: { + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + // Then delete it + await request.delete(`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}`, { + headers: { + Authorization: `SSWS ${connection.credentials.apiToken}` + } + }); + } catch (error: unknown) { + if ( + error instanceof AxiosError && + error.response?.data && + isOktaErrorResponse(error.response.data) && + error.response.data.errorCode === "E0000001" + ) { + // If this is the last secret, we cannot revoke it + return; + } + + throw new BadRequestError({ + message: `Failed to remove client secret with secretId ${secretId} from app ${clientId}: ${createErrorMessage(error)}` + }); + } + }; + + /** + * Issues a new set of credentials. + */ + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotateClientSecret(); + return callback(credentials); + }; + + /** + * Revokes a list of credentials. + */ + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentials, + callback + ) => { + if (!credentials?.length) return callback(); + + for (const { secretId } of credentials) { + await revokeCredential(secretId); + await delayMs(DELAY_MS); + } + return callback(); + }; + + /** + * Rotates credentials by issuing new ones and revoking the old. + */ + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + oldCredentials, + callback, + activeCredentials + ) => { + // Since in Okta you can only have a maximum of 2 secrets at a time, we must delete any other secret besides the current one PRIOR to generating the second secret + if (oldCredentials?.secretId) { + await revokeCredential(oldCredentials.secretId); + } else if (activeCredentials) { + // On the first rotation oldCredentials won't be set so we must find the second secret manually + const secrets = await $listClientSecrets(); + + if (secrets.length > 1) { + const nonActiveSecret = secrets.find((secret) => secret.id !== activeCredentials.secretId); + if (nonActiveSecret) { + await revokeCredential(nonActiveSecret.id); + } + } + } + + const newCredentials = await $rotateClientSecret(); + return callback(newCredentials); + }; + + /** + * Maps the generated credentials into the secret payload format. + */ + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ({ + clientSecret + }) => [ + { key: secretsMapping.clientId, value: clientId }, + { key: secretsMapping.clientSecret, value: clientSecret } + ]; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-schemas.ts new file mode 100644 index 000000000..9325d9518 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-schemas.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const OktaClientSecretRotationGeneratedCredentialsSchema = z + .object({ + clientId: z.string(), + clientSecret: z.string(), + secretId: z.string() + }) + .array() + .min(1) + .max(2); + +const OktaClientSecretRotationParametersSchema = z.object({ + clientId: z + .string() + .trim() + .min(1, "Client ID Required") + .describe(SecretRotations.PARAMETERS.OKTA_CLIENT_SECRET.clientId) +}); + +const OktaClientSecretRotationSecretsMappingSchema = z.object({ + clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.OKTA_CLIENT_SECRET.clientId), + clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.OKTA_CLIENT_SECRET.clientSecret) +}); + +export const OktaClientSecretRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + clientId: z.string(), + clientSecret: z.string() + }) +}); + +export const OktaClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.OktaClientSecret).extend({ + type: z.literal(SecretRotation.OktaClientSecret), + parameters: OktaClientSecretRotationParametersSchema, + secretsMapping: OktaClientSecretRotationSecretsMappingSchema +}); + +export const CreateOktaClientSecretRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.OktaClientSecret +).extend({ + parameters: OktaClientSecretRotationParametersSchema, + secretsMapping: OktaClientSecretRotationSecretsMappingSchema +}); + +export const UpdateOktaClientSecretRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.OktaClientSecret +).extend({ + parameters: OktaClientSecretRotationParametersSchema.optional(), + secretsMapping: OktaClientSecretRotationSecretsMappingSchema.optional() +}); + +export const OktaClientSecretRotationListItemSchema = z.object({ + name: z.literal("Okta Client Secret"), + connection: z.literal(AppConnection.Okta), + type: z.literal(SecretRotation.OktaClientSecret), + template: OktaClientSecretRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-types.ts new file mode 100644 index 000000000..101b4839e --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-types.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { TOktaConnection } from "@app/services/app-connection/okta"; + +import { + CreateOktaClientSecretRotationSchema, + OktaClientSecretRotationGeneratedCredentialsSchema, + OktaClientSecretRotationListItemSchema, + OktaClientSecretRotationSchema +} from "./okta-client-secret-rotation-schemas"; + +export type TOktaClientSecretRotation = z.infer; + +export type TOktaClientSecretRotationInput = z.infer; + +export type TOktaClientSecretRotationListItem = z.infer; + +export type TOktaClientSecretRotationWithConnection = TOktaClientSecretRotation & { + connection: TOktaConnection; +}; + +export type TOktaClientSecretRotationGeneratedCredentials = z.infer< + typeof OktaClientSecretRotationGeneratedCredentialsSchema +>; + +export interface TOktaClientSecretRotationParameters { + clientId: string; + secretId: string; +} + +export interface TOktaClientSecretRotationSecretsMapping { + clientId: string; + clientSecret: string; + secretId: string; +} + +export interface TOktaClientSecret { + id: string; + client_secret: string; +} diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 84dc30821..cf0fe578a 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -6,7 +6,8 @@ export enum SecretRotation { Auth0ClientSecret = "auth0-client-secret", AzureClientSecret = "azure-client-secret", AwsIamUserSecret = "aws-iam-user-secret", - LdapPassword = "ldap-password" + LdapPassword = "ldap-password", + OktaClientSecret = "okta-client-secret" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 228c4c2a1..7c0239add 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -10,6 +10,7 @@ import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret" import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials"; +import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret"; import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; @@ -30,7 +31,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 029c9bdc5..d9a771101 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -9,7 +9,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", [SecretRotation.AzureClientSecret]: "Azure Client Secret", [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", - [SecretRotation.LdapPassword]: "LDAP Password" + [SecretRotation.LdapPassword]: "LDAP Password", + [SecretRotation.OktaClientSecret]: "Okta Client Secret" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { @@ -20,5 +21,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record; appConnectionDAL: Pick; folderCommitService: Pick; + gatewayService: Pick; }; export type TSecretRotationV2ServiceFactory = ReturnType; @@ -126,7 +129,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const appCfg = getConfig(); @@ -218,7 +223,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -269,7 +274,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -315,7 +320,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -380,7 +385,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -424,7 +429,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -461,7 +466,8 @@ export const secretRotationV2ServiceFactory = ({ rotationInterval: payload.rotationInterval } as TSecretRotationV2WithConnection, appConnectionDAL, - kmsService + kmsService, + gatewayService ); // even though we have a db constraint we want to check before any rotation of credentials is attempted @@ -625,7 +631,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -775,7 +781,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -824,7 +830,8 @@ export const secretRotationV2ServiceFactory = ({ connection: appConnection } as TSecretRotationV2WithConnection, appConnectionDAL, - kmsService + kmsService, + gatewayService ); const generatedCredentials = await decryptSecretRotationCredentials({ @@ -907,7 +914,8 @@ export const secretRotationV2ServiceFactory = ({ connection: appConnection } as TSecretRotationV2WithConnection, appConnectionDAL, - kmsService + kmsService, + gatewayService ); const updatedRotation = await rotationFactory.rotateCredentials( @@ -1105,7 +1113,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1152,7 +1160,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1204,7 +1212,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1320,7 +1328,8 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager }); const permissiveFolderMappings = folderMappings.filter(({ path, environment }) => diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index 5547d4582..ab348f172 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -1,4 +1,5 @@ import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { OrderByDirection } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -45,6 +46,13 @@ import { TMySqlCredentialsRotationListItem, TMySqlCredentialsRotationWithConnection } from "./mysql-credentials"; +import { + TOktaClientSecretRotation, + TOktaClientSecretRotationGeneratedCredentials, + TOktaClientSecretRotationInput, + TOktaClientSecretRotationListItem, + TOktaClientSecretRotationWithConnection +} from "./okta-client-secret"; import { TOracleDBCredentialsRotation, TOracleDBCredentialsRotationInput, @@ -68,7 +76,8 @@ export type TSecretRotationV2 = | TAuth0ClientSecretRotation | TAzureClientSecretRotation | TLdapPasswordRotation - | TAwsIamUserSecretRotation; + | TAwsIamUserSecretRotation + | TOktaClientSecretRotation; export type TSecretRotationV2WithConnection = | TPostgresCredentialsRotationWithConnection @@ -78,14 +87,16 @@ export type TSecretRotationV2WithConnection = | TAuth0ClientSecretRotationWithConnection | TAzureClientSecretRotationWithConnection | TLdapPasswordRotationWithConnection - | TAwsIamUserSecretRotationWithConnection; + | TAwsIamUserSecretRotationWithConnection + | TOktaClientSecretRotationWithConnection; export type TSecretRotationV2GeneratedCredentials = | TSqlCredentialsRotationGeneratedCredentials | TAuth0ClientSecretRotationGeneratedCredentials | TAzureClientSecretRotationGeneratedCredentials | TLdapPasswordRotationGeneratedCredentials - | TAwsIamUserSecretRotationGeneratedCredentials; + | TAwsIamUserSecretRotationGeneratedCredentials + | TOktaClientSecretRotationGeneratedCredentials; export type TSecretRotationV2Input = | TPostgresCredentialsRotationInput @@ -95,7 +106,8 @@ export type TSecretRotationV2Input = | TAuth0ClientSecretRotationInput | TAzureClientSecretRotationInput | TLdapPasswordRotationInput - | TAwsIamUserSecretRotationInput; + | TAwsIamUserSecretRotationInput + | TOktaClientSecretRotationInput; export type TSecretRotationV2ListItem = | TPostgresCredentialsRotationListItem @@ -105,7 +117,8 @@ export type TSecretRotationV2ListItem = | TAuth0ClientSecretRotationListItem | TAzureClientSecretRotationListItem | TLdapPasswordRotationListItem - | TAwsIamUserSecretRotationListItem; + | TAwsIamUserSecretRotationListItem + | TOktaClientSecretRotationListItem; export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined; @@ -239,7 +252,8 @@ export type TRotationFactory< > = ( secretRotation: T, appConnectionDAL: Pick, - kmsService: Pick + kmsService: Pick, + gatewayService: Pick ) => { issueCredentials: TRotationFactoryIssueCredentials; revokeCredentials: TRotationFactoryRevokeCredentials; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts index 6dd04d47e..ce5f10bc4 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -6,6 +6,7 @@ import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotatio import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; +import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; @@ -17,5 +18,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [ Auth0ClientSecretRotationSchema, AzureClientSecretRotationSchema, LdapPasswordRotationSchema, - AwsIamUserSecretRotationSchema + AwsIamUserSecretRotationSchema, + OktaClientSecretRotationSchema ]); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 12e9b5964..3e6e5d265 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -1,3 +1,5 @@ +import { Knex } from "knex"; + import { TRotationFactory, TRotationFactoryGetSecretsPayload, @@ -5,7 +7,10 @@ import { TRotationFactoryRevokeCredentials, TRotationFactoryRotateCredentials } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; -import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; +import { + executeWithPotentialGateway, + SQL_CONNECTION_ALTER_LOGIN_STATEMENT +} from "@app/services/app-connection/shared/sql"; import { generatePassword } from "../utils"; import { @@ -30,7 +35,7 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat export const sqlCredentialsRotationFactory: TRotationFactory< TSqlCredentialsRotationWithConnection, TSqlCredentialsRotationGeneratedCredentials -> = (secretRotation) => { +> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => { const { connection, parameters: { username1, username2 }, @@ -38,29 +43,38 @@ export const sqlCredentialsRotationFactory: TRotationFactory< secretsMapping } = secretRotation; - const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { - const client = await getSqlConnectionClient({ - ...connection, - credentials: { - ...connection.credentials, - ...credentials - } - }); + const executeOperation = ( + operation: (client: Knex) => Promise, + credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number] + ) => { + const finalCredentials = { + ...connection.credentials, + ...credentialsOverride + }; + return executeWithPotentialGateway( + { + ...connection, + credentials: finalCredentials + }, + gatewayService, + (client) => operation(client) + ); + }; + + const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { try { - await client.raw("SELECT 1"); + await executeOperation(async (client) => { + await client.raw("SELECT 1"); + }, credentials); } catch (error) { throw new Error(redactPasswords(error, [credentials])); - } finally { - await client.destroy(); } }; const issueCredentials: TRotationFactoryIssueCredentials = async ( callback ) => { - const client = await getSqlConnectionClient(connection); - // For SQL, since we get existing users, we change both their passwords // on issue to invalidate their existing passwords const credentialsSet = [ @@ -69,15 +83,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory< ]; try { - await client.transaction(async (tx) => { - for await (const credentials of credentialsSet) { - await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); - } + await executeOperation(async (client) => { + await client.transaction(async (tx) => { + for await (const credentials of credentialsSet) { + await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } + }); }); } catch (error) { throw new Error(redactPasswords(error, credentialsSet)); - } finally { - await client.destroy(); } for await (const credentials of credentialsSet) { @@ -91,21 +105,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory< credentialsToRevoke, callback ) => { - const client = await getSqlConnectionClient(connection); - const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); try { - await client.transaction(async (tx) => { - for await (const credentials of revokedCredentials) { - // invalidate previous passwords - await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); - } + await executeOperation(async (client) => { + await client.transaction(async (tx) => { + for await (const credentials of revokedCredentials) { + // invalidate previous passwords + await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } + }); }); } catch (error) { throw new Error(redactPasswords(error, revokedCredentials)); - } finally { - await client.destroy(); } return callback(); @@ -115,17 +127,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory< _, callback ) => { - const client = await getSqlConnectionClient(connection); - // generate new password for the next active user const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() }; try { - await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + await executeOperation(async (client) => { + await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + }); } catch (error) { throw new Error(redactPasswords(error, [credentials])); - } finally { - await client.destroy(); } await $validateCredentials(credentials); diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 53056e294..04ece7e5b 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; -import { ProjectVersion, TableName } from "@app/db/schemas"; +import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -66,7 +66,8 @@ export const secretRotationServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretRotationActions.Read, @@ -97,7 +98,8 @@ export const secretRotationServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretRotationActions.Read, @@ -213,7 +215,8 @@ export const secretRotationServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretRotationActions.Read, @@ -263,7 +266,8 @@ export const secretRotationServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretRotationActions.Edit, @@ -283,7 +287,8 @@ export const secretRotationServiceFactory = ({ actorId, projectId: doc.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretRotationActions.Delete, diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts index 137034feb..1550ef41a 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts @@ -567,14 +567,18 @@ export const secretScanningV2QueueServiceFactory = async ({ const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); const project = await projectDAL.findById(projectId); - const projectAdmins = projectMembers.filter((member) => - member.roles.some((role) => role.role === ProjectMembershipRole.Admin) - ); + const recipients = projectMembers.filter((member) => { + const isAdmin = member.roles.some((role) => role.role === ProjectMembershipRole.Admin); + const isCompleted = payload.status === SecretScanningScanStatus.Completed; + // We assume that the committer is one of the project members + const isCommitter = isCompleted && payload.authorEmail === member.user.email; + return isAdmin || isCommitter; + }); const timestamp = new Date().toISOString(); await smtpService.sendMail({ - recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + recipients: recipients.map((member) => member.user.email!).filter(Boolean), template: payload.status === SecretScanningScanStatus.Completed ? SmtpTemplates.SecretScanningV2SecretsDetected diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts index 761059d2a..41da217d8 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { join } from "path"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -94,7 +95,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId }); @@ -156,7 +157,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -201,7 +202,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId }); @@ -235,7 +236,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: payload.projectId }); @@ -348,7 +349,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -401,6 +402,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -474,7 +476,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -538,7 +540,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -583,7 +585,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -626,7 +628,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -669,7 +671,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: dataSource.projectId }); @@ -702,7 +704,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId }); @@ -736,7 +738,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId }); @@ -776,7 +778,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId: finding.projectId }); @@ -807,7 +809,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId }); @@ -842,7 +844,7 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretScanning, projectId }); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index a61b0d586..d60f0f0a0 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -2,7 +2,7 @@ // akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error import { ForbiddenError } from "@casl/ability"; -import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; +import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -103,7 +103,8 @@ export const secretSnapshotServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); @@ -139,7 +140,8 @@ export const secretSnapshotServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); @@ -167,7 +169,8 @@ export const secretSnapshotServiceFactory = ({ actorId, projectId: snapshot.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); @@ -391,7 +394,8 @@ export const secretSnapshotServiceFactory = ({ actorId, projectId: snapshot.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts index e679fdfac..49d8c1ab6 100644 --- a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -58,7 +59,8 @@ export const sshCertificateTemplateServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -130,7 +132,8 @@ export const sshCertificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -198,7 +201,8 @@ export const sshCertificateTemplateServiceFactory = ({ actorId, projectId: certificateTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -224,7 +228,8 @@ export const sshCertificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index fba849d93..aa6d4f66a 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; @@ -79,7 +80,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHostGroups); @@ -171,7 +173,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId: sshHostGroup.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups); @@ -267,7 +270,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId: sshHostGroup.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups); @@ -290,7 +294,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId: sshHostGroup.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SshHostGroups); @@ -316,7 +321,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId: sshHostGroup.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups); @@ -354,7 +360,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId: sshHostGroup.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups); @@ -393,7 +400,8 @@ export const sshHostGroupServiceFactory = ({ actorId, projectId: sshHostGroup.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups); diff --git a/backend/src/ee/services/ssh-host/ssh-host-fns.ts b/backend/src/ee/services/ssh-host/ssh-host-fns.ts index 5b2f98728..dec15e093 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-fns.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-fns.ts @@ -1,5 +1,6 @@ import { Knex } from "knex"; +import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission"; @@ -62,7 +63,8 @@ export const createSshLoginMappings = async ({ userId: user.id, projectId, authMethod: actorAuthMethod, - userOrgId: actorOrgId + userOrgId: actorOrgId, + actionProjectType: ActionProjectType.SSH }); } diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index 36bc1bbb3..d1082b4df 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -111,7 +112,8 @@ export const sshHostServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); const projectHosts = await sshHostDAL.findUserAccessibleSshHosts([project.id], actorId); @@ -144,7 +146,8 @@ export const sshHostServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -273,7 +276,8 @@ export const sshHostServiceFactory = ({ actorId, projectId: host.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -334,7 +338,8 @@ export const sshHostServiceFactory = ({ actorId, projectId: host.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -362,7 +367,8 @@ export const sshHostServiceFactory = ({ actorId, projectId: host.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -401,7 +407,8 @@ export const sshHostServiceFactory = ({ actorId, projectId: host.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); const internalPrincipals = await convertActorToPrincipals({ @@ -520,7 +527,8 @@ export const sshHostServiceFactory = ({ actorId, projectId: host.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index 2e45c836d..6c35f0ddd 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; @@ -72,7 +73,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -107,7 +109,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -175,7 +178,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -213,7 +217,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -254,7 +259,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId: sshCertificateTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -375,7 +381,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId: sshCertificateTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -472,7 +479,8 @@ export const sshCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts index 69e7e5e1d..6b9686e25 100644 --- a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts +++ b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -35,7 +36,8 @@ export const trustedIpServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); const trustedIps = await trustedIpDAL.find({ @@ -59,7 +61,8 @@ export const trustedIpServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); @@ -104,7 +107,8 @@ export const trustedIpServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); @@ -149,7 +153,8 @@ export const trustedIpServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 584f480ad..ac4ee26fb 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2289,6 +2289,10 @@ export const AppConnections = { SUPABASE: { accessKey: "The Key used to access Supabase.", instanceUrl: "The URL used to access Supabase." + }, + OKTA: { + instanceUrl: "The URL used to access your Okta organization.", + apiToken: "The API token used to authenticate with Okta." } } }; @@ -2502,6 +2506,11 @@ export const SecretSyncs = { SUPABASE: { projectId: "The ID of the Supabase project to sync secrets to.", projectName: "The name of the Supabase project to sync secrets to." + }, + BITBUCKET: { + workspaceSlug: "The Bitbucket Workspace slug to sync secrets to.", + repositorySlug: "The Bitbucket Repository slug to sync secrets to.", + environmentId: "The Bitbucket Deployment Environment uuid to sync secrets to." } } }; @@ -2594,6 +2603,9 @@ export const SecretRotations = { AWS_IAM_USER_SECRET: { userName: "The name of the client to rotate credentials for.", region: "The AWS region the client is present in." + }, + OKTA_CLIENT_SECRET: { + clientId: "The ID of the Okta Application to rotate the client secret for." } }, SECRETS_MAPPING: { @@ -2616,6 +2628,10 @@ export const SecretRotations = { AWS_IAM_USER_SECRET: { accessKeyId: "The name of the secret that the access key ID will be mapped to.", secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to." + }, + OKTA_CLIENT_SECRET: { + clientId: "The name of the secret that the client ID will be mapped to.", + clientSecret: "The name of the secret that the rotated client secret will be mapped to." } } }; diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts index 9a986efbb..967e7e007 100644 --- a/backend/src/lib/crypto/cryptography/crypto.ts +++ b/backend/src/lib/crypto/cryptography/crypto.ts @@ -93,7 +93,13 @@ const cryptographyFactory = () => { }; const verifyFipsLicense = (licenseService: Pick) => { - if (isFipsModeEnabled({ skipInitializationCheck: true }) && !licenseService.onPremFeatures?.fips) { + const appCfg = getConfig(); + + if ( + !appCfg.isDevelopmentMode && + isFipsModeEnabled({ skipInitializationCheck: true }) && + !licenseService.onPremFeatures?.fips + ) { throw new CryptographyError({ message: "FIPS mode is enabled but your license does not include FIPS support. Please contact support." }); diff --git a/backend/src/server/lib/cookie.ts b/backend/src/server/lib/cookie.ts index cc6956056..323bf4be9 100644 --- a/backend/src/server/lib/cookie.ts +++ b/backend/src/server/lib/cookie.ts @@ -3,6 +3,11 @@ import { FastifyReply } from "fastify"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +/** + * `aod` (Auth Origin Domain) cookie is used to store the origin domain of the application when user was last authenticated. + * This is useful for determining the target domain for authentication redirects, especially in cloud deployments. + * It is set only in cloud mode to ensure that the cookie is shared across subdomains. + */ export function addAuthOriginDomainCookie(res: FastifyReply) { try { const appCfg = getConfig(); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4b28105ab..ea4cf9676 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1555,7 +1555,12 @@ export const registerRoutes = async ( folderService, permissionService, folderDAL, - projectEnvDAL + projectEnvDAL, + secretApprovalRequestService, + secretApprovalPolicyService, + projectDAL, + secretV2BridgeService, + folderCommitDAL }); const identityOidcAuthService = identityOidcAuthServiceFactory({ @@ -1729,7 +1734,9 @@ export const registerRoutes = async ( appConnectionDAL, permissionService, kmsService, - licenseService + licenseService, + gatewayService, + gatewayDAL }); const secretSyncService = secretSyncServiceFactory({ @@ -1827,7 +1834,8 @@ export const registerRoutes = async ( snapshotService, secretQueueService, queueService, - appConnectionDAL + appConnectionDAL, + gatewayService }); const certificateAuthorityService = certificateAuthorityServiceFactory({ diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index 0bc8f7c59..50111b109 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -25,12 +25,14 @@ export const registerAppConnectionEndpoints = ; updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null; isPlatformManagedCredentials?: boolean; + gatewayId?: string | null; }>; sanitizedResponseSchema: z.ZodTypeAny; }) => { @@ -224,10 +226,10 @@ export const registerAppConnectionEndpoints = { - const { name, method, credentials, description, isPlatformManagedCredentials } = req.body; + const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body; const appConnection = (await server.services.appConnection.createAppConnection( - { name, method, app, credentials, description, isPlatformManagedCredentials }, + { name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId }, req.permission )) as T; @@ -270,11 +272,11 @@ export const registerAppConnectionEndpoints = { - const { name, credentials, description, isPlatformManagedCredentials } = req.body; + const { name, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body; const { connectionId } = req.params; const appConnection = (await server.services.appConnection.updateAppConnection( - { name, credentials, connectionId, description, isPlatformManagedCredentials }, + { name, credentials, connectionId, description, isPlatformManagedCredentials, gatewayId }, req.permission )) as T; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 7c1b52edd..ba0826f87 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -71,6 +71,7 @@ import { import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql"; +import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta"; import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema @@ -138,7 +139,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedZabbixConnectionSchema.options, ...SanitizedRailwayConnectionSchema.options, ...SanitizedChecklyConnectionSchema.options, - ...SanitizedSupabaseConnectionSchema.options + ...SanitizedSupabaseConnectionSchema.options, + ...SanitizedOktaConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -175,7 +177,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ ZabbixConnectionListItemSchema, RailwayConnectionListItemSchema, ChecklyConnectionListItemSchema, - SupabaseConnectionListItemSchema + SupabaseConnectionListItemSchema, + OktaConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts index 7fe5113e5..23381e65b 100644 --- a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -85,4 +85,40 @@ export const registerBitbucketConnectionRouter = async (server: FastifyZodProvid return { repositories }; } }); + + server.route({ + method: "GET", + url: `/:connectionId/environments`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + workspaceSlug: z.string().min(1).max(255), + repositorySlug: z.string().min(1).max(255) + }), + response: { + 200: z.object({ + environments: z.object({ slug: z.string(), name: z.string(), uuid: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId }, + query: { workspaceSlug, repositorySlug } + } = req; + + const environments = await server.services.appConnection.bitbucket.listEnvironments( + { connectionId, workspaceSlug, repositorySlug }, + req.permission + ); + + return { environments }; + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 287a406f6..3bbdc363d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -25,6 +25,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; +import { registerOktaConnectionRouter } from "./okta-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRenderConnectionRouter } from "./render-connection-router"; @@ -72,5 +73,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.Okta, + server, + sanitizedResponseSchema: SanitizedOktaConnectionSchema, + createSchema: CreateOktaConnectionSchema, + updateSchema: UpdateOktaConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/apps`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + apps: z.object({ id: z.string(), label: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId } + } = req; + + const apps = await server.services.appConnection.okta.listApps(connectionId, req.permission); + return { apps }; + } + }); +}; diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index e7c06ff51..a91548285 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -42,6 +42,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { maxAge: 0 }); + void res.cookie("aod", "", { + httpOnly: false, + path: "/", + sameSite: "lax", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); + return { message: "Successfully logged out" }; } }); diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 2015842a5..05aade960 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -158,7 +158,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { includeRoles: z .enum(["true", "false"]) .default("false") - .transform((value) => value === "true") + .transform((value) => value === "true"), + type: z.nativeEnum(ProjectType).optional() }), response: { 200: z.object({ @@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + type: req.query.type }); return { workspaces }; } @@ -1050,6 +1052,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { body: z.object({ limit: z.number().default(100), offset: z.number().default(0), + type: z.nativeEnum(ProjectType).optional(), orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME), orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC), name: z diff --git a/backend/src/server/routes/v1/secret-sync-routers/bitbucket-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/bitbucket-sync-router.ts new file mode 100644 index 000000000..17cd0dbbf --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/bitbucket-sync-router.ts @@ -0,0 +1,17 @@ +import { + BitbucketSyncSchema, + CreateBitbucketSyncSchema, + UpdateBitbucketSyncSchema +} from "@app/services/secret-sync/bitbucket"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerBitbucketSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Bitbucket, + server, + responseSchema: BitbucketSyncSchema, + createSchema: CreateBitbucketSyncSchema, + updateSchema: UpdateBitbucketSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 8e8f696b7..1b640acae 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -7,6 +7,7 @@ import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync- import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; +import { registerBitbucketSyncRouter } from "./bitbucket-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerChecklySyncRouter } from "./checkly-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; @@ -57,5 +58,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 233ce0ea8..7fcdc7217 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -32,7 +32,8 @@ export enum AppConnection { Railway = "railway", Bitbucket = "bitbucket", Checkly = "checkly", - Supabase = "supabase" + Supabase = "supabase", + Okta = "okta" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 10bab521e..9568761f7 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -5,6 +5,7 @@ import { validateOCIConnectionCredentials } from "@app/ee/services/app-connections/oci"; import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; @@ -91,6 +92,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; +import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { RenderConnectionMethod } from "./render/render-connection-enums"; @@ -154,7 +156,8 @@ export const listAppConnectionOptions = () => { getRailwayConnectionListItem(), getBitbucketConnectionListItem(), getChecklyConnectionListItem(), - getSupabaseConnectionListItem() + getSupabaseConnectionListItem(), + getOktaConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -201,7 +204,8 @@ export const decryptAppConnectionCredentials = async ({ }; export const validateAppConnectionCredentials = async ( - appConnection: TAppConnectionConfig + appConnection: TAppConnectionConfig, + gatewayService: Pick ): Promise => { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, @@ -239,10 +243,11 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator }; - return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); + return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService); }; export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { @@ -278,6 +283,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case CloudflareConnectionMethod.APIToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case OktaConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -365,7 +371,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Checkly]: platformManagedCredentialsNotSupported, - [AppConnection.Supabase]: platformManagedCredentialsNotSupported + [AppConnection.Supabase]: platformManagedCredentialsNotSupported, + [AppConnection.Okta]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 8a85020d8..03b979312 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -34,7 +34,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Railway]: "Railway", [AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Checkly]: "Checkly", - [AppConnection.Supabase]: "Supabase" + [AppConnection.Supabase]: "Supabase", + [AppConnection.Okta]: "Okta" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -71,5 +72,6 @@ export const APP_CONNECTION_PLAN_MAP: Record z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name), @@ -30,12 +30,23 @@ export const GenericCreateAppConnectionFieldsSchema = ( .describe(AppConnections.CREATE(app).description), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) - : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) + : z + .literal(false, { + errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + }) + .optional() + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), + gatewayId: supportsGateways + ? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") + : z + .undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + .or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })) + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); export const GenericUpdateAppConnectionFieldsSchema = ( app: AppConnection, - { supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {} + { supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {} ) => z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(), @@ -47,5 +58,16 @@ export const GenericUpdateAppConnectionFieldsSchema = ( .describe(AppConnections.UPDATE(app).description), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials) - : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) + : z + .literal(false, { + errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + }) + .optional() + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), + gatewayId: supportsGateways + ? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") + : z + .undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + .or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })) + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 1f3e76f8e..86be7ac8d 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -3,8 +3,14 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + OrgPermissionAppConnectionActions, + OrgPermissionGatewayActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -73,6 +79,8 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMySqlConnectionCredentialsSchema } from "./mysql"; +import { ValidateOktaConnectionCredentialsSchema } from "./okta"; +import { oktaConnectionService } from "./okta/okta-connection-service"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidateRailwayConnectionCredentialsSchema } from "./railway"; import { railwayConnectionService } from "./railway/railway-connection-service"; @@ -96,6 +104,8 @@ export type TAppConnectionServiceFactoryDep = { permissionService: Pick; kmsService: Pick; licenseService: Pick; + gatewayService: Pick; + gatewayDAL: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -134,14 +144,17 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const { permission } = await permissionService.getOrgPermission( @@ -222,7 +235,7 @@ export const appConnectionServiceFactory = ({ }; const createAppConnection = async ( - { method, app, credentials, ...params }: TCreateAppConnectionDTO, + { method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO, actor: OrgServiceActor ) => { const { permission } = await permissionService.getOrgPermission( @@ -238,6 +251,20 @@ export const appConnectionServiceFactory = ({ OrgPermissionSubjects.AppConnections ); + if (gatewayId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway) { + throw new NotFoundError({ + message: `Gateway with ID ${gatewayId} not found for org` + }); + } + } + await enterpriseAppCheck( licenseService, app, @@ -245,12 +272,16 @@ export const appConnectionServiceFactory = ({ "Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections." ); - const validatedCredentials = await validateAppConnectionCredentials({ - app, - credentials, - method, - orgId: actor.orgId - } as TAppConnectionConfig); + const validatedCredentials = await validateAppConnectionCredentials( + { + app, + credentials, + method, + orgId: actor.orgId, + gatewayId + } as TAppConnectionConfig, + gatewayService + ); try { const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => { @@ -265,6 +296,7 @@ export const appConnectionServiceFactory = ({ encryptedCredentials, method, app, + gatewayId, ...params }); }; @@ -277,9 +309,11 @@ export const appConnectionServiceFactory = ({ app, orgId: actor.orgId, credentials: validatedCredentials, - method + method, + gatewayId } as TAppConnectionConfig, - (platformCredentials) => createConnection(platformCredentials) + (platformCredentials) => createConnection(platformCredentials), + gatewayService ); } else { connection = await createConnection(validatedCredentials); @@ -300,7 +334,7 @@ export const appConnectionServiceFactory = ({ }; const updateAppConnection = async ( - { connectionId, credentials, ...params }: TUpdateAppConnectionDTO, + { connectionId, credentials, gatewayId, ...params }: TUpdateAppConnectionDTO, actor: OrgServiceActor ) => { const appConnection = await appConnectionDAL.findById(connectionId); @@ -327,6 +361,22 @@ export const appConnectionServiceFactory = ({ OrgPermissionSubjects.AppConnections ); + if (gatewayId !== appConnection.gatewayId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + + if (gatewayId) { + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway) { + throw new NotFoundError({ + message: `Gateway with ID ${gatewayId} not found for org` + }); + } + } + } + // prevent updating credentials or management status if platform managed if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) { throw new BadRequestError({ @@ -351,12 +401,16 @@ export const appConnectionServiceFactory = ({ } Connection with method ${getAppConnectionMethodName(method)}` }); - updatedCredentials = await validateAppConnectionCredentials({ - app, - orgId: actor.orgId, - credentials, - method - } as TAppConnectionConfig); + updatedCredentials = await validateAppConnectionCredentials( + { + app, + orgId: actor.orgId, + credentials, + method, + gatewayId + } as TAppConnectionConfig, + gatewayService + ); if (!updatedCredentials) throw new BadRequestError({ message: "Unable to validate connection - check credentials" }); @@ -375,6 +429,7 @@ export const appConnectionServiceFactory = ({ return appConnectionDAL.updateById(connectionId, { orgId: actor.orgId, encryptedCredentials, + gatewayId, ...params }); }; @@ -391,9 +446,11 @@ export const appConnectionServiceFactory = ({ app, orgId: actor.orgId, credentials: updatedCredentials, - method + method, + gatewayId } as TAppConnectionConfig, - (platformCredentials) => updateConnection(platformCredentials) + (platformCredentials) => updateConnection(platformCredentials), + gatewayService ); } else { updatedConnection = await updateConnection(updatedCredentials); @@ -549,6 +606,7 @@ export const appConnectionServiceFactory = ({ railway: railwayConnectionService(connectAppConnectionById), bitbucket: bitbucketConnectionService(connectAppConnectionById), checkly: checklyConnectionService(connectAppConnectionById), - supabase: supabaseConnectionService(connectAppConnectionById) + supabase: supabaseConnectionService(connectAppConnectionById), + okta: oktaConnectionService(connectAppConnectionById) }; }; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index bb6be0a70..6a0c27b7f 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -9,6 +9,7 @@ import { TOracleDBConnectionInput, TValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -142,6 +143,12 @@ import { } from "./ldap"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql"; +import { + TOktaConnection, + TOktaConnectionConfig, + TOktaConnectionInput, + TValidateOktaConnectionCredentialsSchema +} from "./okta"; import { TPostgresConnection, TPostgresConnectionInput, @@ -231,6 +238,7 @@ export type TAppConnection = { id: string } & ( | TRailwayConnection | TChecklyConnection | TSupabaseConnection + | TOktaConnection ); export type TAppConnectionRaw = NonNullable>>; @@ -272,6 +280,7 @@ export type TAppConnectionInput = { id: string } & ( | TRailwayConnectionInput | TChecklyConnectionInput | TSupabaseConnectionInput + | TOktaConnectionInput ); export type TSqlConnectionInput = @@ -282,7 +291,7 @@ export type TSqlConnectionInput = export type TCreateAppConnectionDTO = Pick< TAppConnectionInput, - "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" + "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" >; export type TUpdateAppConnectionDTO = Partial> & { @@ -320,7 +329,8 @@ export type TAppConnectionConfig = | TZabbixConnectionConfig | TRailwayConnectionConfig | TChecklyConnectionConfig - | TSupabaseConnectionConfig; + | TSupabaseConnectionConfig + | TOktaConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -356,7 +366,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateZabbixConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema | TValidateChecklyConnectionCredentialsSchema - | TValidateSupabaseConnectionCredentialsSchema; + | TValidateSupabaseConnectionCredentialsSchema + | TValidateOktaConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; @@ -369,14 +380,17 @@ export type TListAwsConnectionIamUsers = { }; export type TAppConnectionCredentialsValidator = ( - appConnection: TAppConnectionConfig + appConnection: TAppConnectionConfig, + gatewayService: Pick ) => Promise; export type TAppConnectionTransitionCredentialsToPlatform = ( appConnection: TAppConnectionConfig, - callback: (credentials: TAppConnection["credentials"]) => Promise + callback: (credentials: TAppConnection["credentials"]) => Promise, + gatewayService: Pick ) => Promise; export type TAppConnectionBaseConfig = { supportsPlatformManagedCredentials?: boolean; + supportsGateways?: boolean; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts index 2d418a8a3..4abf879f8 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -9,6 +9,7 @@ import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; import { TBitbucketConnection, TBitbucketConnectionConfig, + TBitbucketEnvironment, TBitbucketRepo, TBitbucketWorkspace } from "./bitbucket-connection-types"; @@ -21,11 +22,15 @@ export const getBitbucketConnectionListItem = () => { }; }; +export const createAuthHeader = (email: string, apiToken: string): string => { + return `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; +}; + export const getBitbucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => { try { const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, { headers: { - Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Authorization: createAuthHeader(email, apiToken), Accept: "application/json" } }); @@ -57,7 +62,7 @@ export const listBitbucketWorkspaces = async (appConnection: TBitbucketConnectio const { email, apiToken } = appConnection.credentials; const headers = { - Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Authorization: createAuthHeader(email, apiToken), Accept: "application/json" }; @@ -89,7 +94,7 @@ export const listBitbucketRepositories = async (appConnection: TBitbucketConnect const { email, apiToken } = appConnection.credentials; const headers = { - Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Authorization: createAuthHeader(email, apiToken), Accept: "application/json" }; @@ -115,3 +120,43 @@ export const listBitbucketRepositories = async (appConnection: TBitbucketConnect return allRepos; }; + +export const listBitbucketEnvironments = async ( + appConnection: TBitbucketConnection, + workspaceSlug: string, + repositorySlug: string +) => { + const { email, apiToken } = appConnection.credentials; + + const headers = { + Authorization: createAuthHeader(email, apiToken), + Accept: "application/json" + }; + + const environments: TBitbucketEnvironment[] = []; + let hasNextPage = true; + + let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspaceSlug)}/${encodeURIComponent(repositorySlug)}/environments?pagelen=100`; + + let iterationCount = 0; + // Limit to 10 iterations, fetching at most 10 * 100 = 1000 environments + while (hasNextPage && iterationCount < 10) { + // eslint-disable-next-line no-await-in-loop + const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, { + headers + }); + + if (data?.values.length > 0) { + environments.push(...data.values); + } + + if (data.next) { + environmentsUrl = data.next; + } else { + hasNextPage = false; + } + iterationCount += 1; + } + + return environments; +}; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts index f08a8d276..b995cb9c0 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -1,8 +1,16 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns"; -import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types"; +import { + listBitbucketEnvironments, + listBitbucketRepositories, + listBitbucketWorkspaces +} from "./bitbucket-connection-fns"; +import { + TBitbucketConnection, + TGetBitbucketEnvironmentsDTO, + TGetBitbucketRepositoriesDTO +} from "./bitbucket-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, @@ -26,8 +34,18 @@ export const bitbucketConnectionService = (getAppConnection: TGetAppConnectionFu return repositories; }; + const listEnvironments = async ( + { connectionId, workspaceSlug, repositorySlug }: TGetBitbucketEnvironmentsDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const environments = await listBitbucketEnvironments(appConnection, workspaceSlug, repositorySlug); + return environments; + }; + return { listWorkspaces, - listRepositories + listRepositories, + listEnvironments }; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts index b0694c6e3..40af42321 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -38,3 +38,20 @@ export type TBitbucketRepo = { full_name: string; // workspace-slug/repo-slug slug: string; }; + +export type TGetBitbucketEnvironmentsDTO = { + connectionId: string; + workspaceSlug: string; + repositorySlug: string; +}; + +export type TBitbucketEnvironment = { + uuid: string; + slug: string; + name: string; +}; + +export type TBitbucketEnvironmentsResponse = { + values: TBitbucketEnvironment[]; + next?: string; +}; diff --git a/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts b/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts index 84d7a1ce1..fba852a0a 100644 --- a/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts +++ b/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts @@ -9,6 +9,7 @@ import { getAppConnectionMethodName } from "@app/services/app-connection/app-con import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { AppConnection } from "../app-connection-enums"; +import { GithubTokenRespData, isGithubErrorResponse } from "../github/github-connection-fns"; import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums"; import { TGitHubRadarConnection, @@ -71,13 +72,6 @@ export const listGitHubRadarRepositories = async (appConnection: TGitHubRadarCon return repositories; }; -type TokenRespData = { - access_token: string; - scope: string; - token_type: string; - error?: string; -}; - export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => { const { credentials, method } = config; @@ -93,10 +87,10 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa }); } - let tokenResp: AxiosResponse; + let tokenResp: AxiosResponse; try { - tokenResp = await request.get("https://github.com/login/oauth/access_token", { + tokenResp = await request.get("https://github.com/login/oauth/access_token", { params: { client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID, client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET, @@ -108,19 +102,27 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa "Accept-Encoding": "application/json" } }); + + if (isGithubErrorResponse(tokenResp?.data)) { + throw new BadRequestError({ + message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}` + }); + } } catch (e: unknown) { + if (e instanceof BadRequestError) { + throw e; + } + throw new BadRequestError({ message: `Unable to validate connection: verify credentials` }); } - if (tokenResp.status !== 200) { - throw new BadRequestError({ - message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.` - }); - } - if (method === GitHubRadarConnectionMethod.App) { + if (!tokenResp.data.access_token) { + throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` }); + } + const installationsResp = await request.get<{ installations: { id: number; @@ -149,10 +151,6 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa } } - if (!tokenResp.data.access_token) { - throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` }); - } - switch (method) { case GitHubRadarConnectionMethod.App: return { diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index e4281625b..360923e19 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -144,14 +144,14 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow } }; -type TokenRespData = { +export type GithubTokenRespData = { access_token?: string; scope: string; token_type: string; error?: string; }; -function isErrorResponse(data: TokenRespData): data is TokenRespData & { +export function isGithubErrorResponse(data: GithubTokenRespData): data is GithubTokenRespData & { error: string; error_description: string; error_uri: string; @@ -191,10 +191,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect }); } - let tokenResp: AxiosResponse; + let tokenResp: AxiosResponse; try { - tokenResp = await request.get("https://github.com/login/oauth/access_token", { + tokenResp = await request.get("https://github.com/login/oauth/access_token", { params: { client_id: clientId, client_secret: clientSecret, @@ -207,7 +207,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect } }); - if (isErrorResponse(tokenResp?.data)) { + if (isGithubErrorResponse(tokenResp?.data)) { throw new BadRequestError({ message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}` }); diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index 994f9a40d..f8d380949 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -49,7 +49,10 @@ export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("me ]); export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdateMsSqlConnectionSchema = z @@ -58,7 +61,12 @@ export const UpdateMsSqlConnectionSchema = z AppConnections.UPDATE(AppConnection.MsSql).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const MsSqlConnectionListItemSchema = z.object({ name: z.literal("Microsoft SQL Server"), diff --git a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts index 082bac557..51a533395 100644 --- a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts +++ b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts @@ -47,7 +47,10 @@ export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("me ]); export const CreateMySqlConnectionSchema = ValidateMySqlConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdateMySqlConnectionSchema = z @@ -56,7 +59,12 @@ export const UpdateMySqlConnectionSchema = z AppConnections.UPDATE(AppConnection.MySql).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const MySqlConnectionListItemSchema = z.object({ name: z.literal("MySQL"), diff --git a/backend/src/services/app-connection/okta/index.ts b/backend/src/services/app-connection/okta/index.ts new file mode 100644 index 000000000..ce06fe7e2 --- /dev/null +++ b/backend/src/services/app-connection/okta/index.ts @@ -0,0 +1,4 @@ +export * from "./okta-connection-enums"; +export * from "./okta-connection-fns"; +export * from "./okta-connection-schemas"; +export * from "./okta-connection-types"; diff --git a/backend/src/services/app-connection/okta/okta-connection-enums.ts b/backend/src/services/app-connection/okta/okta-connection-enums.ts new file mode 100644 index 000000000..75bd5ea61 --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-enums.ts @@ -0,0 +1,3 @@ +export enum OktaConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/okta/okta-connection-fns.ts b/backend/src/services/app-connection/okta/okta-connection-fns.ts new file mode 100644 index 000000000..a48eebbb8 --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-fns.ts @@ -0,0 +1,57 @@ +import { request } from "@app/lib/config/request"; +import { UnauthorizedError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { OktaConnectionMethod } from "./okta-connection-enums"; +import { TOktaApp, TOktaConnection, TOktaConnectionConfig } from "./okta-connection-types"; + +export const getOktaConnectionListItem = () => { + return { + name: "Okta" as const, + app: AppConnection.Okta as const, + methods: Object.values(OktaConnectionMethod) as [OktaConnectionMethod.ApiToken] + }; +}; + +export const getOktaInstanceUrl = async (config: TOktaConnectionConfig) => { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + await blockLocalAndPrivateIpAddresses(instanceUrl); + return instanceUrl; +}; + +export const validateOktaConnectionCredentials = async (config: TOktaConnectionConfig) => { + const { apiToken } = config.credentials; + const instanceUrl = await getOktaInstanceUrl(config); + + try { + await request.get(`${instanceUrl}/api/v1/users/me`, { + headers: { + Accept: "application/json", + Authorization: `SSWS ${apiToken}` + }, + validateStatus: (status) => status === 200 + }); + } catch (error: unknown) { + throw new UnauthorizedError({ + message: "Unable to validate connection: invalid credentials" + }); + } + + return config.credentials; +}; + +export const listOktaApps = async (appConnection: TOktaConnection) => { + const { apiToken } = appConnection.credentials; + const instanceUrl = await getOktaInstanceUrl(appConnection); + + const { data } = await request.get(`${instanceUrl}/api/v1/apps`, { + headers: { + Accept: "application/json", + Authorization: `SSWS ${apiToken}` + } + }); + + return data.filter((app) => app.status === "ACTIVE" && app.name === "oidc_client"); +}; diff --git a/backend/src/services/app-connection/okta/okta-connection-schemas.ts b/backend/src/services/app-connection/okta/okta-connection-schemas.ts new file mode 100644 index 000000000..37ce0ec11 --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-schemas.ts @@ -0,0 +1,69 @@ +import RE2 from "re2"; +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { OktaConnectionMethod } from "./okta-connection-enums"; + +export const OktaConnectionApiTokenCredentialsSchema = z.object({ + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .min(1, "Instance URL required") + .max(255) + .describe(AppConnections.CREDENTIALS.OKTA.instanceUrl), + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .refine((value) => new RE2("^00[a-zA-Z0-9_-]{40}$").test(value), "Invalid Okta API Token format") + .describe(AppConnections.CREDENTIALS.OKTA.apiToken) +}); + +const BaseOktaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Okta) }); + +export const OktaConnectionSchema = BaseOktaConnectionSchema.extend({ + method: z.literal(OktaConnectionMethod.ApiToken), + credentials: OktaConnectionApiTokenCredentialsSchema +}); + +export const SanitizedOktaConnectionSchema = z.discriminatedUnion("method", [ + BaseOktaConnectionSchema.extend({ + method: z.literal(OktaConnectionMethod.ApiToken), + credentials: OktaConnectionApiTokenCredentialsSchema.pick({ + instanceUrl: true + }) + }) +]); + +export const ValidateOktaConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(OktaConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Okta).method), + credentials: OktaConnectionApiTokenCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Okta).credentials) + }) +]); + +export const CreateOktaConnectionSchema = ValidateOktaConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Okta) +); + +export const UpdateOktaConnectionSchema = z + .object({ + credentials: OktaConnectionApiTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Okta).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Okta)); + +export const OktaConnectionListItemSchema = z.object({ + name: z.literal("Okta"), + app: z.literal(AppConnection.Okta), + methods: z.nativeEnum(OktaConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/okta/okta-connection-service.ts b/backend/src/services/app-connection/okta/okta-connection-service.ts new file mode 100644 index 000000000..8ac036dcd --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-service.ts @@ -0,0 +1,23 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listOktaApps } from "./okta-connection-fns"; +import { TOktaConnection } from "./okta-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const oktaConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Okta, connectionId, actor); + const apps = await listOktaApps(appConnection); + return apps; + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/okta/okta-connection-types.ts b/backend/src/services/app-connection/okta/okta-connection-types.ts new file mode 100644 index 000000000..8ed53aaed --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-types.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateOktaConnectionSchema, + OktaConnectionSchema, + ValidateOktaConnectionCredentialsSchema +} from "./okta-connection-schemas"; + +export type TOktaConnection = z.infer; + +export type TOktaConnectionInput = z.infer & { + app: AppConnection.Okta; +}; + +export type TValidateOktaConnectionCredentialsSchema = typeof ValidateOktaConnectionCredentialsSchema; + +export type TOktaConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TOktaApp = { + id: string; + label: string; + status: "ACTIVE" | "INACTIVE"; + name: "oidc_client"; // "oidc_client" or other types +}; diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index 1ddf1e2da..da74bd669 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -47,7 +47,10 @@ export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion( ]); export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdatePostgresConnectionSchema = z @@ -56,7 +59,12 @@ export const UpdatePostgresConnectionSchema = z AppConnections.UPDATE(AppConnection.Postgres).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const PostgresConnectionListItemSchema = z.object({ name: z.literal("PostgreSQL"), diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index 33cc8257d..d9adc91dd 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -1,11 +1,13 @@ import knex, { Knex } from "knex"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types"; @@ -98,25 +100,80 @@ export const getSqlConnectionClient = async (appConnection: Pick { - const { credentials, app } = config; +export const executeWithPotentialGateway = async ( + config: TSqlConnectionConfig, + gatewayService: Pick, + operation: (client: Knex) => Promise +): Promise => { + const { credentials, app, gatewayId } = config; - let client: Knex | undefined; + if (gatewayId && gatewayService) { + const [targetHost] = await verifyHostInputValidity(credentials.host, true); + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + return withGatewayProxy( + async (proxyPort) => { + const client = knex({ + client: SQL_CONNECTION_CLIENT_MAP[app], + connection: { + database: credentials.database, + port: proxyPort, + host: "localhost", + user: credentials.username, + password: credentials.password, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ...getConnectionConfig({ app, credentials }) + } + }); + try { + return await operation(client); + } finally { + await client.destroy(); + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + targetHost, + targetPort: credentials.port, + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); + } + + // Non-gateway path + const client = await getSqlConnectionClient({ app, credentials }); try { - client = await getSqlConnectionClient({ app, credentials }); + return await operation(client); + } finally { + await client.destroy(); + } +}; - await client.raw(`Select 1`); - - return credentials; +export const validateSqlConnectionCredentials = async ( + config: TSqlConnectionConfig, + gatewayService: Pick +) => { + try { + await executeWithPotentialGateway(config, gatewayService, async (client) => { + await client.raw(`Select 1`); + }); + return config.credentials; } catch (error) { throw new BadRequestError({ message: `Unable to validate connection: ${ - (error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials" + (error as Error)?.message?.replaceAll(config.credentials.password, "********************") ?? + "verify credentials" }` }); - } finally { - await client?.destroy(); } }; @@ -132,22 +189,23 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record< export const transferSqlConnectionCredentialsToPlatform = async ( config: TSqlConnectionConfig, - callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise + callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise, + gatewayService: Pick ) => { const { credentials, app } = config; - const client = await getSqlConnectionClient({ app, credentials }); - const newPassword = alphaNumericNanoId(32); try { - return await client.transaction(async (tx) => { - await tx.raw( - ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) - ); - return callback({ - ...credentials, - password: newPassword + return await executeWithPotentialGateway(config, gatewayService, (client) => { + return client.transaction(async (tx) => { + await tx.raw( + ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) + ); + return callback({ + ...credentials, + password: newPassword + }); }); }); } catch (error) { @@ -161,7 +219,5 @@ export const transferSqlConnectionCredentialsToPlatform = async ( (error as Error)?.message?.replaceAll(newPassword, "********************") ?? "Encountered an error transferring credentials to platform" }); - } finally { - await client.destroy(); } }; diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-types.ts b/backend/src/services/app-connection/shared/sql/sql-connection-types.ts index bbfe4086c..104aacfb9 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-types.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-types.ts @@ -1,6 +1,9 @@ import { DiscriminativePick } from "@app/lib/types"; import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types"; -export type TSqlConnectionConfig = DiscriminativePick & { +export type TSqlConnectionConfig = DiscriminativePick< + TSqlConnectionInput, + "method" | "app" | "credentials" | "gatewayId" +> & { orgId: string; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 0f30e91c3..fa0fe017f 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { TableName } from "@app/db/schemas"; +import { ActionProjectType, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -100,7 +100,8 @@ export const certificateAuthorityServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -167,7 +168,8 @@ export const certificateAuthorityServiceFactory = ({ actorId: actor.id, projectId: certificateAuthority.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -215,7 +217,8 @@ export const certificateAuthorityServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -268,7 +271,8 @@ export const certificateAuthorityServiceFactory = ({ actorId: actor.id, projectId: certificateAuthority.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -341,7 +345,8 @@ export const certificateAuthorityServiceFactory = ({ actorId: actor.id, projectId: certificateAuthority.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 80201eab6..dd30cc62e 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -4,7 +4,7 @@ import * as x509 from "@peculiar/x509"; import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; +import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, @@ -150,7 +150,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId: dto.actorId, projectId, actorAuthMethod: dto.actorAuthMethod, - actorOrgId: dto.actorOrgId + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -333,7 +334,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -357,7 +359,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId: dto.actorId, projectId: ca.projectId, actorAuthMethod: dto.actorAuthMethod, - actorOrgId: dto.actorOrgId + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -389,7 +392,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -414,7 +418,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -477,7 +482,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -763,7 +769,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -799,7 +806,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -879,7 +887,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1026,7 +1035,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1197,7 +1207,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1553,7 +1564,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId: dto.actorId, projectId: ca.projectId, actorAuthMethod: dto.actorAuthMethod, - actorOrgId: dto.actorOrgId + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1920,7 +1932,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); const certificateTemplates = await certificateTemplateDAL.find({ caId }); diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index f8e1cf788..20c061bf7 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; +import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -76,7 +76,8 @@ export const certificateTemplateServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -137,7 +138,8 @@ export const certificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -201,7 +203,8 @@ export const certificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -227,7 +230,8 @@ export const certificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -268,7 +272,8 @@ export const certificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -350,7 +355,8 @@ export const certificateTemplateServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -429,7 +435,8 @@ export const certificateTemplateServiceFactory = ({ actorId: dto.actorId, projectId: certTemplate.projectId, actorAuthMethod: dto.actorAuthMethod, - actorOrgId: dto.actorOrgId + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 541bddac7..de6da16ee 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -79,7 +80,8 @@ export const certificateServiceFactory = ({ actorId, projectId: cert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -109,7 +111,8 @@ export const certificateServiceFactory = ({ actorId, projectId: cert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -142,7 +145,8 @@ export const certificateServiceFactory = ({ actorId, projectId: cert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -191,7 +195,8 @@ export const certificateServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -239,7 +244,8 @@ export const certificateServiceFactory = ({ actorId, projectId: cert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -319,7 +325,8 @@ export const certificateServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -523,7 +530,8 @@ export const certificateServiceFactory = ({ actorId, projectId: cert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index 7817266b3..f913f972f 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { SigningAlgorithm } from "@app/lib/crypto/sign"; @@ -38,7 +39,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek); @@ -77,7 +79,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek); @@ -113,7 +116,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek); @@ -129,7 +133,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); @@ -151,7 +156,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); @@ -172,7 +178,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); @@ -194,7 +201,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek); @@ -221,7 +229,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); @@ -268,7 +277,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); @@ -291,7 +301,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek); @@ -325,7 +336,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek); @@ -360,7 +372,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actorId: actor.id, projectId: key.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek); diff --git a/backend/src/services/folder-commit/folder-commit-service.ts b/backend/src/services/folder-commit/folder-commit-service.ts index 3576f444b..470edbbba 100644 --- a/backend/src/services/folder-commit/folder-commit-service.ts +++ b/backend/src/services/folder-commit/folder-commit-service.ts @@ -2,7 +2,13 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { TSecretFolders, TSecretFolderVersions, TSecretV2TagJunctionInsert, TSecretVersionsV2 } from "@app/db/schemas"; +import { + ActionProjectType, + TSecretFolders, + TSecretFolderVersions, + TSecretV2TagJunctionInsert, + TSecretVersionsV2 +} from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -47,6 +53,14 @@ export enum ResourceType { FOLDER = "folder" } +export type TCommitResourceChangeDTO = { + type: string; + secretVersionId?: string; + folderVersionId?: string; + isUpdate?: boolean; + folderId?: string; +}; + type TCreateCommitDTO = { actor: { type: string; @@ -57,13 +71,7 @@ type TCreateCommitDTO = { }; message?: string; folderId: string; - changes: { - type: string; - secretVersionId?: string; - folderVersionId?: string; - isUpdate?: boolean; - folderId?: string; - }[]; + changes: TCommitResourceChangeDTO[]; omitIgnoreFilter?: boolean; }; @@ -217,7 +225,8 @@ export const folderCommitServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits); @@ -2060,7 +2069,8 @@ export const folderCommitServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index a04d8b19f..50a08e94f 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { constructPermissionErrorMessage, @@ -78,7 +78,8 @@ export const groupProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); @@ -271,7 +272,8 @@ export const groupProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); @@ -384,7 +386,8 @@ export const groupProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); @@ -428,7 +431,8 @@ export const groupProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); @@ -455,7 +459,8 @@ export const groupProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); @@ -496,7 +501,8 @@ export const groupProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index d0ef6fd88..098bdcf9a 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -37,7 +37,7 @@ export const validateAccountIds = z export const validatePrincipalArns = z .string() .trim() - .max(2048) + .max(4096) .default("") // Custom validation for ARN format .refine( diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 7ee051d88..4f0964f42 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation @@ -62,7 +62,8 @@ export const identityProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Create, @@ -181,7 +182,8 @@ export const identityProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, @@ -291,7 +293,8 @@ export const identityProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Delete, @@ -319,7 +322,8 @@ export const identityProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, @@ -352,7 +356,8 @@ export const identityProjectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( @@ -388,7 +393,8 @@ export const identityProjectServiceFactory = ({ actorId, projectId: membership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 0729fcb5d..248488e9f 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -4,7 +4,13 @@ import { Octokit } from "@octokit/rest"; import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client"; import AWS from "aws-sdk"; -import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; +import { + ActionProjectType, + SecretEncryptionAlgo, + SecretKeyEncoding, + TIntegrationAuths, + TIntegrationAuthsInsert +} from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -97,7 +103,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const authorizations = await integrationAuthDAL.find({ projectId }); @@ -115,7 +122,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: auth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null; @@ -138,7 +146,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); return integrationAuth; @@ -163,7 +172,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); @@ -281,7 +291,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); @@ -435,7 +446,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); @@ -732,7 +744,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -766,7 +779,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -796,7 +810,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -837,7 +852,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -865,7 +881,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -939,7 +956,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -986,7 +1004,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1020,7 +1039,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1078,7 +1098,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1114,7 +1135,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1155,7 +1177,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1195,7 +1218,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1235,7 +1259,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1274,7 +1299,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1314,7 +1340,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1382,7 +1409,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1456,7 +1484,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1506,7 +1535,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1554,7 +1584,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1622,7 +1653,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1663,7 +1695,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1775,7 +1808,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); @@ -1798,7 +1832,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); @@ -1831,7 +1866,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(sourcePermission).throwUnlessCan( @@ -1844,7 +1880,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(targetPermission).throwUnlessCan( @@ -1877,7 +1914,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -1911,7 +1949,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -1951,7 +1990,8 @@ export const integrationAuthServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index e03ca1e8f..2ef8615eb 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -90,7 +91,8 @@ export const integrationServiceFactory = ({ actorId, projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); @@ -165,7 +167,8 @@ export const integrationServiceFactory = ({ actorId, projectId: integration.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); @@ -228,7 +231,8 @@ export const integrationServiceFactory = ({ actorId, projectId: integration.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -255,7 +259,8 @@ export const integrationServiceFactory = ({ actorId, projectId: integration.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -297,7 +302,8 @@ export const integrationServiceFactory = ({ actorId, projectId: integration.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); @@ -333,7 +339,8 @@ export const integrationServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -352,7 +359,8 @@ export const integrationServiceFactory = ({ actorId, projectId: integration.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index cdbe9550f..5f60bfe1e 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -3,6 +3,7 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { + ActionProjectType, OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, @@ -981,7 +982,8 @@ export const orgServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(projectPermission).throwUnlessCan( ProjectPermissionMemberActions.Create, diff --git a/backend/src/services/pki-alert/pki-alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts index 8b348085f..c35bfbd16 100644 --- a/backend/src/services/pki-alert/pki-alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -78,7 +79,8 @@ export const pkiAlertServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); @@ -107,7 +109,8 @@ export const pkiAlertServiceFactory = ({ actorId, projectId: alert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); @@ -133,7 +136,8 @@ export const pkiAlertServiceFactory = ({ actorId, projectId: alert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); @@ -165,7 +169,8 @@ export const pkiAlertServiceFactory = ({ actorId, projectId: alert.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts); diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index 7c89ce255..0f3c26556 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { TPkiCollectionItems } from "@app/db/schemas"; +import { ActionProjectType, TPkiCollectionItems } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -55,7 +55,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -87,7 +88,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); @@ -111,7 +113,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); @@ -138,7 +141,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -167,7 +171,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); @@ -210,7 +215,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -297,7 +303,8 @@ export const pkiCollectionServiceFactory = ({ actorId, projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 245337296..a3e6ec78c 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -119,7 +120,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -181,7 +183,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -234,7 +237,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -296,7 +300,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -332,7 +337,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -387,7 +393,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -433,7 +440,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -691,7 +699,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -738,7 +747,8 @@ export const pkiSubscriberServiceFactory = ({ actorId, projectId: subscriber.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/pki-templates/pki-templates-service.ts b/backend/src/services/pki-templates/pki-templates-service.ts index 98469c157..e648ab88f 100644 --- a/backend/src/services/pki-templates/pki-templates-service.ts +++ b/backend/src/services/pki-templates/pki-templates-service.ts @@ -3,6 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import RE2 from "re2"; +import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -118,7 +119,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId: ca.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -170,7 +172,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -233,7 +236,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -265,7 +269,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -290,7 +295,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); const certTemplate = await pkiTemplatesDAL.find({ projectId }, { limit, offset, count: true }); @@ -332,7 +338,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -378,7 +385,8 @@ export const pkiTemplatesServiceFactory = ({ actorId, projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 76c40dff7..c6625c28a 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectVersion } from "@app/db/schemas"; +import { ActionProjectType, ProjectVersion } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -45,7 +45,8 @@ export const projectBotServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -115,7 +116,8 @@ export const projectBotServiceFactory = ({ actorId, projectId: bot.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index 6773ee600..9a82a6bbe 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -46,7 +47,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); @@ -134,7 +136,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); @@ -197,7 +200,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); @@ -252,7 +256,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId: environment.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index c4eae9e2e..a884d25bc 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; @@ -36,7 +37,8 @@ export const projectKeyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); @@ -65,7 +67,8 @@ export const projectKeyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId); return latestKey; @@ -83,7 +86,8 @@ export const projectKeyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); return projectKeyDAL.findAllProjectUserPubKeys(projectId); diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 9cef4dabf..b9e502922 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; -import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { constructPermissionErrorMessage, @@ -90,7 +90,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); @@ -133,7 +134,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); @@ -155,7 +157,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); @@ -181,7 +184,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); const orgMembers = await orgDAL.findMembership({ @@ -261,7 +265,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); @@ -370,7 +375,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); @@ -412,7 +418,8 @@ export const projectMembershipServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); diff --git a/backend/src/services/project-role/project-role-fns.ts b/backend/src/services/project-role/project-role-fns.ts index bf5044f47..4dfcf960b 100644 --- a/backend/src/services/project-role/project-role-fns.ts +++ b/backend/src/services/project-role/project-role-fns.ts @@ -11,7 +11,7 @@ import { } from "@app/ee/services/permission/default-roles"; import { TGetPredefinedRolesDTO } from "@app/services/project-role/project-role-types"; -export const getPredefinedRoles = ({ projectId, roleFilter }: TGetPredefinedRolesDTO) => { +export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetPredefinedRolesDTO) => { return [ { id: uuidv4(), @@ -75,5 +75,5 @@ export const getPredefinedRoles = ({ projectId, roleFilter }: TGetPredefinedRole createdAt: new Date(), updatedAt: new Date() } - ].filter(({ slug }) => !roleFilter || roleFilter === 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 76613805e..dd0eecc68 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 { ProjectMembershipRole, TableName, TProjects } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, @@ -71,7 +71,8 @@ export const projectRoleServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); @@ -111,12 +112,14 @@ export const projectRoleServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + 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: project.id, + projectType: project.type as ProjectType, roleFilter: roleSlug as ProjectMembershipRole }); @@ -139,7 +142,8 @@ export const projectRoleServiceFactory = ({ actorId, projectId: projectRole.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); @@ -169,7 +173,8 @@ export const projectRoleServiceFactory = ({ actorId, projectId: projectRole.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); @@ -210,14 +215,18 @@ export const projectRoleServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); const customRoles = await projectRoleDAL.find( { projectId: project.id }, { sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] } ); - const roles = [...getPredefinedRoles({ projectId: project.id }), ...(customRoles || [])]; + const roles = [ + ...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }), + ...(customRoles || []) + ]; return roles; }; @@ -233,7 +242,8 @@ export const projectRoleServiceFactory = ({ actorId: userId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); // just to satisfy ts if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" }); diff --git a/backend/src/services/project-role/project-role-types.ts b/backend/src/services/project-role/project-role-types.ts index 37395a9a7..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 { ProjectMembershipRole, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export enum ProjectRoleServiceIdentifierType { @@ -37,5 +37,6 @@ export type TListRolesDTO = { export type TGetPredefinedRolesDTO = { projectId: string; + projectType: ProjectType; roleFilter?: ProjectMembershipRole; }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index bd008a5be..2766db379 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -3,6 +3,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { ProjectsSchema, + ProjectType, ProjectUpgradeStatus, ProjectVersion, SortDirection, @@ -21,12 +22,17 @@ export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { const projectOrm = ormify(db, TableName.Project); - const findIdentityProjects = async (identityId: string, orgId: string) => { + const findIdentityProjects = async (identityId: string, orgId: string, projectType?: ProjectType) => { try { const workspaces = await db(TableName.IdentityProjectMembership) .where({ identityId }) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -66,13 +72,18 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findUserProjects = async (userId: string, orgId: string) => { + const findUserProjects = async (userId: string, orgId: string, projectType?: ProjectType) => { try { const workspaces = await db .replicaNode()(TableName.ProjectMembership) .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -92,6 +103,11 @@ export const projectDALFactory = (db: TDbClient) => { .whereIn("groupId", groups) .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .whereNotIn( `${TableName.Project}.id`, workspaces.map(({ id }) => id) @@ -161,12 +177,17 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findAllProjectsByIdentity = async (identityId: string) => { + const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => { try { const workspaces = await db .replicaNode()(TableName.IdentityProjectMembership) .where({ identityId }) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -372,6 +393,7 @@ export const projectDALFactory = (db: TDbClient) => { orgId: string; actor: ActorType; actorId: string; + type?: ProjectType; limit?: number; offset?: number; name?: string; @@ -426,6 +448,9 @@ export const projectDALFactory = (db: TDbClient) => { void query.orderBy([{ column: `${TableName.Project}.name`, order: sortDir }]); } + if (dto.type) { + void query.where(`${TableName.Project}.type`, dto.type); + } if (dto.name) { void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`); } diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 75c8cdd25..3a8b2b132 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,7 +1,14 @@ import { ForbiddenError, subject } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { ProjectMembershipRole, ProjectVersion, TableName, TProjectEnvironments } from "@app/db/schemas"; +import { + ActionProjectType, + ProjectMembershipRole, + ProjectType, + ProjectVersion, + TableName, + TProjectEnvironments +} from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; @@ -242,7 +249,8 @@ export const projectServiceFactory = ({ kmsKeyId, tx: trx, createDefaultEnvs = true, - template = InfisicalProjectTemplate.Default + template = InfisicalProjectTemplate.Default, + type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); const { permission, membership: orgMembership } = await permissionService.getOrgPermission( @@ -258,7 +266,11 @@ export const projectServiceFactory = ({ await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]); const plan = await licenseService.getPlan(organization.id); - if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) { + if ( + plan.workspaceLimit !== null && + plan.workspacesUsed >= plan.workspaceLimit && + type === ProjectType.SecretManager + ) { // case: limit imposed on number of workspaces allowed // case: number of workspaces used exceeds the number of workspaces allowed throw new BadRequestError({ @@ -295,6 +307,7 @@ export const projectServiceFactory = ({ const project = await projectDAL.create( { name: workspaceName, + type, description: workspaceDescription, orgId: organization.id, slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), @@ -305,14 +318,16 @@ export const projectServiceFactory = ({ tx ); - await bootstrapSshProject({ - projectId: project.id, - sshCertificateAuthorityDAL, - sshCertificateAuthoritySecretDAL, - kmsService, - projectSshConfigDAL, - tx - }); + if (type === ProjectType.SSH) { + await bootstrapSshProject({ + projectId: project.id, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService, + projectSshConfigDAL, + tx + }); + } // set ghost user as admin of project const projectMembership = await projectMembershipDAL.create( @@ -512,7 +527,8 @@ export const projectServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); @@ -570,11 +586,11 @@ export const projectServiceFactory = ({ return deletedProject; }; - const getProjects = async ({ actorId, actor, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => { + const getProjects = async ({ actorId, actor, includeRoles, actorAuthMethod, actorOrgId, type }: TListProjectsDTO) => { const workspaces = actor === ActorType.IDENTITY - ? await projectDAL.findIdentityProjects(actorId, actorOrgId) - : await projectDAL.findUserProjects(actorId, actorOrgId); + ? await projectDAL.findIdentityProjects(actorId, actorOrgId, type) + : await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { const { permission } = await permissionService.getUserOrgPermission( @@ -598,7 +614,10 @@ export const projectServiceFactory = ({ workspaces.map(async (workspace) => { return { ...workspace, - roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles({ projectId: workspace.id })] + roles: [ + ...(workspaceMappedToRoles[workspace.id] || []), + ...getPredefinedRoles({ projectId: workspace.id, projectType: workspace.type as ProjectType }) + ] }; }) ); @@ -617,7 +636,8 @@ export const projectServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); return project; }; @@ -630,7 +650,8 @@ export const projectServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -674,7 +695,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -699,7 +721,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -728,7 +751,8 @@ export const projectServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -759,7 +783,8 @@ export const projectServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); if (!hasRole(ProjectMembershipRole.Admin)) { @@ -791,7 +816,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -812,7 +838,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); @@ -882,7 +909,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -920,7 +948,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -966,7 +995,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1010,7 +1040,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); @@ -1037,7 +1068,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); @@ -1064,7 +1096,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); const allowedSubscribers = []; @@ -1102,7 +1135,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager }); const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId); @@ -1132,7 +1166,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -1165,7 +1200,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); const allowedHosts = []; @@ -1204,7 +1240,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups); @@ -1231,7 +1268,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); @@ -1269,7 +1307,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan( @@ -1303,7 +1342,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); @@ -1330,7 +1370,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); @@ -1359,7 +1400,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); @@ -1381,7 +1423,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); if (!membership) { @@ -1413,7 +1456,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); @@ -1452,7 +1496,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SSH }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -1535,7 +1580,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); @@ -1607,7 +1653,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -1684,7 +1731,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -1807,7 +1855,8 @@ export const projectServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); @@ -1835,7 +1884,15 @@ export const projectServiceFactory = ({ }); }; - const searchProjects = async ({ name, offset, permission, limit, orderBy, orderDirection }: TSearchProjectsDTO) => { + const searchProjects = async ({ + name, + offset, + permission, + limit, + type, + orderBy, + orderDirection + }: TSearchProjectsDTO) => { // check user belong to org await permissionService.getOrgPermission( permission.type, @@ -1849,6 +1906,7 @@ export const projectServiceFactory = ({ limit, offset, name, + type, orgId: permission.orgId, actor: permission.type, actorId: permission.id, @@ -1872,7 +1930,7 @@ export const projectServiceFactory = ({ actor: permission.type, actorId: permission.id, projectId, - + actionProjectType: ActionProjectType.Any, actorAuthMethod: permission.authMethod, actorOrgId: permission.orgId }) diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 5d4578194..b8c37a858 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -108,7 +108,7 @@ export type TDeleteProjectDTO = { export type TListProjectsDTO = { includeRoles: boolean; - type?: ProjectType | "all"; + type?: ProjectType; } & Omit; export type TUpgradeProjectDTO = { diff --git a/backend/src/services/secret-blind-index/secret-blind-index-service.ts b/backend/src/services/secret-blind-index/secret-blind-index-service.ts index c8fed2a2b..a19ce8b88 100644 --- a/backend/src/services/secret-blind-index/secret-blind-index-service.ts +++ b/backend/src/services/secret-blind-index/secret-blind-index-service.ts @@ -1,4 +1,4 @@ -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -36,7 +36,8 @@ export const secretBlindIndexServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const secretCount = await secretBlindIndexDAL.countOfSecretsWithNullSecretBlindIndex(projectId); @@ -55,7 +56,8 @@ export const secretBlindIndexServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Insufficient privileges, user must be admin" }); @@ -78,7 +80,8 @@ export const secretBlindIndexServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Insufficient privileges, user must be admin" }); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index e8d0b05e5..a60d29348 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -1,8 +1,10 @@ +/* eslint-disable no-await-in-loop */ import { ForbiddenError, subject } from "@casl/ability"; +import { Knex } from "knex"; import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; -import { TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; +import { ActionProjectType, TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -12,14 +14,21 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns"; -import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { + ChangeType, + CommitType, + TCommitResourceChangeDTO, + TFolderCommitServiceFactory +} from "../folder-commit/folder-commit-service"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal"; import { TCreateFolderDTO, + TCreateManyFoldersDTO, TDeleteFolderDTO, + TDeleteManyFoldersDTO, TGetFolderByIdDTO, TGetFolderDTO, TGetFoldersDeepByEnvsDTO, @@ -69,7 +78,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -236,21 +246,32 @@ export const secretFolderServiceFactory = ({ actor, actorId, projectSlug, + projectId: providedProjectId, actorAuthMethod, actorOrgId, - folders - }: TUpdateManyFoldersDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) { - throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); + folders, + tx: providedTx, + commitChanges + }: TUpdateManyFoldersDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[]; projectId?: string }) => { + let projectId = providedProjectId; + if (!projectId && projectSlug) { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) { + throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); + } + projectId = project.id; + } + if (!projectId) { + throw new BadRequestError({ message: "Must provide either project slug or projectId" }); } const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId: project.id, + projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); folders.forEach(({ environment, path: secretPath }) => { @@ -260,12 +281,12 @@ export const secretFolderServiceFactory = ({ ); }); - const result = await folderDAL.transaction(async (tx) => - Promise.all( + const executeBulkUpdate = async (tx: Knex) => { + return Promise.all( folders.map(async (newFolder) => { const { environment, path: secretPath, id, name, description } = newFolder; - const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath); + const parentFolder = await folderDAL.findBySecretPath(projectId as string, environment, secretPath, tx); if (!parentFolder) { throw new NotFoundError({ message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`, @@ -273,10 +294,10 @@ export const secretFolderServiceFactory = ({ }); } - const env = await projectEnvDAL.findOne({ projectId: project.id, slug: environment }); + const env = await projectEnvDAL.findOne({ projectId, slug: environment }, tx); if (!env) { throw new NotFoundError({ - message: `Environment with slug '${environment}' in project with ID '${project.id}' not found`, + message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`, name: "UpdateManyFolders" }); } @@ -323,26 +344,34 @@ export const secretFolderServiceFactory = ({ }, tx ); - await folderCommitService.createCommit( - { - actor: { - type: actor, - metadata: { - id: actorId - } + if (commitChanges) { + commitChanges.push({ + type: CommitType.ADD, + isUpdate: true, + folderVersionId: folderVersion.id + }); + } else { + await folderCommitService.createCommit( + { + actor: { + type: actor, + metadata: { + id: actorId + } + }, + message: "Folder updated", + folderId: parentFolder.id, + changes: [ + { + type: CommitType.ADD, + isUpdate: true, + folderVersionId: folderVersion.id + } + ] }, - message: "Folder updated", - folderId: parentFolder.id, - changes: [ - { - type: CommitType.ADD, - isUpdate: true, - folderVersionId: folderVersion.id - } - ] - }, - tx - ); + tx + ); + } if (!doc) { throw new NotFoundError({ message: `Failed to update folder with id '${id}', not found`, @@ -352,13 +381,16 @@ export const secretFolderServiceFactory = ({ return { oldFolder: folder, newFolder: doc }; }) - ) - ); + ); + }; + + // Execute with provided transaction or create new one + const result = providedTx ? await executeBulkUpdate(providedTx) : await folderDAL.transaction(executeBulkUpdate); await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string))); return { - projectId: project.id, + projectId, newFolders: result.map((res) => res.newFolder), oldFolders: result.map((res) => res.oldFolder) }; @@ -381,7 +413,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -582,7 +615,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -688,7 +722,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); @@ -756,7 +791,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -797,7 +833,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -832,7 +869,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId: folder.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); @@ -860,7 +898,8 @@ export const secretFolderServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -887,7 +926,8 @@ export const secretFolderServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager }); const environments = await projectEnvDAL.find({ projectId }); @@ -974,6 +1014,363 @@ export const secretFolderServiceFactory = ({ })); }; + const createManyFolders = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + folders, + tx: providedTx, + commitChanges + }: TCreateManyFoldersDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + folders.forEach(({ environment, path: secretPath }) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + }); + + const foldersByEnv = folders.reduce( + (acc, folder) => { + if (!acc[folder.environment]) { + acc[folder.environment] = []; + } + acc[folder.environment].push(folder); + return acc; + }, + {} as Record + ); + + const executeBulkCreate = async (tx: Knex) => { + const createdFolders = []; + + for (const [environment, envFolders] of Object.entries(foldersByEnv)) { + const env = await projectEnvDAL.findOne({ projectId, slug: environment }); + if (!env) { + throw new NotFoundError({ + message: `Environment with slug '${environment}' in project with ID '${projectId}' not found` + }); + } + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateFolder(env.id, env.projectId)]); + + for (const folderSpec of envFolders) { + const { name, path: secretPath, description } = folderSpec; + + const pathWithFolder = path.join(secretPath, name); + const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx); + + if (!parentFolder) { + throw new NotFoundError({ + message: `Parent folder for path '${pathWithFolder}' not found` + }); + } + + // Check if the exact folder already exists + const existingFolder = await folderDAL.findOne( + { + envId: env.id, + parentId: parentFolder.id, + name, + isReserved: false + }, + tx + ); + + if (existingFolder) { + createdFolders.push(existingFolder); + // eslint-disable-next-line no-continue + continue; + } + + // Handle exact folder case + if (parentFolder.path === pathWithFolder) { + createdFolders.push(parentFolder); + // eslint-disable-next-line no-continue + continue; + } + + let currentParentId = parentFolder.id; + + // Build the full path we need by processing each segment + if (parentFolder.path !== secretPath) { + const missingSegments = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean); + const newFolders: TSecretFoldersInsert[] = []; + + for (const segment of missingSegments) { + const existingSegment = await folderDAL.findOne( + { + name: segment, + parentId: currentParentId, + envId: env.id, + isReserved: false + }, + tx + ); + + if (existingSegment) { + currentParentId = existingSegment.id; + } else { + const newFolder = { + name: segment, + parentId: currentParentId, + id: uuidv4(), + envId: env.id, + version: 1 + }; + + currentParentId = newFolder.id; + newFolders.push(newFolder); + } + } + + if (newFolders.length) { + const docs = await folderDAL.insertMany(newFolders, tx); + const folderVersions = await folderVersionDAL.insertMany( + docs.map((doc) => ({ + name: doc.name, + envId: doc.envId, + version: doc.version, + folderId: doc.id, + description: doc.description + })), + tx + ); + await folderCommitService.createCommit( + { + actor: { + type: actor, + metadata: { + id: actorId + } + }, + message: "Folders created (batch)", + folderId: currentParentId, + changes: folderVersions.map((fv) => ({ + type: CommitType.ADD, + folderVersionId: fv.id + })) + }, + tx + ); + } + } + + // Create the target folder + const doc = await folderDAL.create( + { name, envId: env.id, version: 1, parentId: currentParentId, description }, + tx + ); + + const folderVersion = await folderVersionDAL.create( + { + name: doc.name, + envId: doc.envId, + version: doc.version, + folderId: doc.id, + description: doc.description + }, + tx + ); + + if (commitChanges) { + commitChanges.push({ + type: CommitType.ADD, + folderVersionId: folderVersion.id + }); + } else { + await folderCommitService.createCommit( + { + actor: { + type: actor, + metadata: { + id: actorId + } + }, + message: "Folder created (batch)", + folderId: doc.id, + changes: [ + { + type: CommitType.ADD, + folderVersionId: folderVersion.id + } + ] + }, + tx + ); + } + + createdFolders.push(doc); + } + } + + return createdFolders; + }; + const result = providedTx ? await executeBulkCreate(providedTx) : await folderDAL.transaction(executeBulkCreate); + const uniqueParentIds = [...new Set(result.map((folder) => folder.parentId).filter(Boolean))]; + await Promise.all(uniqueParentIds.map((parentId) => snapshotService.performSnapshot(parentId as string))); + + return { + folders: result, + count: result.length + }; + }; + + const deleteManyFolders = async ({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + folders, + tx: providedTx, + commitChanges + }: TDeleteManyFoldersDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + folders.forEach(({ environment, path: secretPath }) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + }); + + const foldersByEnv = folders.reduce( + (acc, folder) => { + if (!acc[folder.environment]) { + acc[folder.environment] = []; + } + acc[folder.environment].push(folder); + return acc; + }, + {} as Record + ); + + const executeBulkDelete = async (tx: Knex) => { + const deletedFolders = []; + + for (const [environment, envFolders] of Object.entries(foldersByEnv)) { + const env = await projectEnvDAL.findOne({ projectId, slug: environment }); + if (!env) { + throw new NotFoundError({ + message: `Environment with slug '${environment}' not found` + }); + } + + for (const folderSpec of envFolders) { + const { path: secretPath, idOrName } = folderSpec; + + const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx); + if (!parentFolder) { + throw new NotFoundError({ + message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found` + }); + } + + await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName }); + + let folderToDelete = await folderDAL + .findOne({ + envId: env.id, + name: idOrName, + parentId: parentFolder.id, + isReserved: false + }) + .catch(() => null); + + if (!folderToDelete && uuidValidate(idOrName)) { + folderToDelete = await folderDAL + .findOne({ + envId: env.id, + id: idOrName, + parentId: parentFolder.id, + isReserved: false + }) + .catch(() => null); + } + + if (!folderToDelete) { + throw new NotFoundError({ + message: `Folder with ID/name '${idOrName}' not found` + }); + } + + const [doc] = await folderDAL.delete( + { + envId: env.id, + id: folderToDelete.id, + parentId: parentFolder.id, + isReserved: false + }, + tx + ); + + const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id], tx); + + if (commitChanges) { + commitChanges.push({ + type: CommitType.DELETE, + folderVersionId: folderVersions[doc.id].id, + folderId: doc.id + }); + } else { + await folderCommitService.createCommit( + { + actor: { + type: actor, + metadata: { + id: actorId + } + }, + message: "Folder deleted (batch)", + folderId: parentFolder.id, + changes: [ + { + type: CommitType.DELETE, + folderVersionId: folderVersions[doc.id].id, + folderId: doc.id + } + ] + }, + tx + ); + } + + deletedFolders.push(doc); + } + } + + return deletedFolders; + }; + + const result = providedTx ? await executeBulkDelete(providedTx) : await folderDAL.transaction(executeBulkDelete); + + const uniqueParentIds = [...new Set(result.map((folder) => folder.parentId).filter(Boolean))]; + await Promise.all(uniqueParentIds.map((parentId) => snapshotService.performSnapshot(parentId as string))); + + return { + folders: result, + count: result.length + }; + }; + return { createFolder, updateFolder, @@ -986,6 +1383,8 @@ export const secretFolderServiceFactory = ({ getFoldersDeepByEnvs, getProjectEnvironmentsFolders, getFolderVersionsByIds, - getFolderVersions + getFolderVersions, + createManyFolders, + deleteManyFolders }; }; diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index 4008676db..ae8e2c5dc 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -1,6 +1,8 @@ import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; + export enum ReservedFolders { SecretReplication = "__reserve_replication_" } @@ -21,7 +23,7 @@ export type TUpdateFolderDTO = { } & TProjectPermission; export type TUpdateManyFoldersDTO = { - projectSlug: string; + projectSlug?: string; folders: { environment: string; path: string; @@ -62,3 +64,30 @@ export type TGetFoldersDeepByEnvsDTO = { export type TFindFoldersDeepByParentIdsDTO = { parentIds: string[]; }; + +export type TCreateManyFoldersDTO = { + projectId: string; + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId?: string; + folders: Array<{ + name: string; + environment: string; + path: string; + description?: string | null; + }>; +}; + +export type TDeleteManyFoldersDTO = { + projectId: string; + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId?: string; + folders: Array<{ + environment: string; + path: string; + idOrName: string; + }>; +}; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 297c5d01f..403484fc2 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { ForbiddenError, subject } from "@casl/ability"; -import { TableName } from "@app/db/schemas"; +import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { hasSecretReadValueOrDescribePermission, @@ -87,7 +87,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); // check if user has permission to import into destination path @@ -204,7 +205,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -301,7 +303,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -375,7 +378,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); // check if user has permission to import into destination path @@ -451,7 +455,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -484,7 +489,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const filteredEnvironments = []; for (const environment of environments) { @@ -537,7 +543,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -586,7 +593,8 @@ export const secretImportServiceFactory = ({ actorId, projectId: folder.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -634,7 +642,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -669,7 +678,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -752,7 +762,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const filteredEnvironments = []; for (const environment of environments) { @@ -804,7 +815,8 @@ export const secretImportServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( permission.cannot( diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-constants.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-constants.ts new file mode 100644 index 000000000..121d3910d --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const BITBUCKET_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Bitbucket", + destination: SecretSync.Bitbucket, + connection: AppConnection.Bitbucket, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts new file mode 100644 index 000000000..93b285c79 --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts @@ -0,0 +1,222 @@ +import { request } from "@app/lib/config/request"; +import { createAuthHeader } from "@app/services/app-connection/bitbucket"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { + TBitbucketListVariables, + TBitbucketSyncWithCredentials, + TBitbucketVariable, + TDeleteBitbucketVariable, + TPutBitbucketVariable +} from "@app/services/secret-sync/bitbucket/bitbucket-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; + +const buildVariablesUrl = (workspace: string, repository: string, environment?: string, uuid?: string): string => { + const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`; + + if (environment) { + return `${baseUrl}/deployments_config/environments/${environment}/variables/${uuid || ""}`; + } + + return `${baseUrl}/pipelines_config/variables/${uuid || ""}`; +}; + +const listVariables = async ({ + workspaceSlug, + repositorySlug, + environmentId, + authHeader +}: TBitbucketListVariables): Promise => { + const url = buildVariablesUrl(workspaceSlug, repositorySlug, environmentId); + + const { data } = await request.get<{ values: TBitbucketVariable[] }>(url, { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + }); + + return data.values; +}; + +const upsertVariable = async ({ + workspaceSlug, + repositorySlug, + environmentId, + key, + value, + existingVariables, + authHeader +}: { + workspaceSlug: string; + repositorySlug: string; + environmentId?: string; + key: string; + value: string; + existingVariables: TBitbucketVariable[]; + authHeader: string; +}) => { + const existingVariable = existingVariables.find((variable) => variable.key === key); + const requestData = { key, value, secured: true }; + const headers = { + Authorization: authHeader, + "Content-Type": "application/json" + }; + + if (existingVariable) { + const url = buildVariablesUrl(workspaceSlug, repositorySlug, environmentId, existingVariable.uuid); + return request.put(url, requestData, { headers }); + } + + const url = buildVariablesUrl(workspaceSlug, repositorySlug, environmentId); + return request.post(url, requestData, { headers }); +}; + +const putVariables = async ({ + workspaceSlug, + repositorySlug, + environmentId, + secretMap, + authHeader +}: TPutBitbucketVariable & { secretMap: TSecretMap; authHeader: string }) => { + const existingVariables = await listVariables({ + workspaceSlug, + repositorySlug, + environmentId, + authHeader + }); + + const promises = Object.entries(secretMap).map(([key, { value }]) => + upsertVariable({ + workspaceSlug, + repositorySlug, + environmentId, + key, + value, + existingVariables, + authHeader + }) + ); + + return Promise.all(promises); +}; + +const deleteVariables = async ({ + workspaceSlug, + repositorySlug, + environmentId, + keys, + authHeader +}: TDeleteBitbucketVariable) => { + const existingVariables = await listVariables({ + workspaceSlug, + repositorySlug, + environmentId, + authHeader + }); + + const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key)); + const promises = variablesToDelete.map((variable) => { + const url = buildVariablesUrl(workspaceSlug, repositorySlug, environmentId, variable.uuid); + return request.delete(url, { + headers: { Authorization: authHeader } + }); + }); + + return Promise.all(promises); +}; + +export const BitbucketSyncFns = { + syncSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + environment, + destinationConfig: { workspaceSlug, repositorySlug, environmentId } + } = secretSync; + + const { email, apiToken } = connection.credentials; + const authHeader = createAuthHeader(email, apiToken); + + try { + await putVariables({ + workspaceSlug, + repositorySlug, + environmentId, + secretMap, + authHeader + }); + } catch (error) { + throw new SecretSyncError({ error }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + try { + const existingVariables = await listVariables({ + workspaceSlug, + repositorySlug, + environmentId, + authHeader + }); + + const keysToDelete = existingVariables + .map((variable) => variable.key) + .filter( + (secret) => + matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap) + ); + + if (keysToDelete.length > 0) { + await deleteVariables({ + workspaceSlug, + repositorySlug, + environmentId, + keys: keysToDelete, + authHeader + }); + } + } catch (error) { + throw new SecretSyncError({ error }); + } + }, + + removeSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { workspaceSlug, repositorySlug, environmentId } + } = secretSync; + + const { email, apiToken } = connection.credentials; + const authHeader = createAuthHeader(email, apiToken); + + try { + const existingVariables = await listVariables({ + workspaceSlug, + repositorySlug, + environmentId, + authHeader + }); + + const keysToRemove = existingVariables.map((variable) => variable.key).filter((secret) => secret in secretMap); + + if (keysToRemove.length > 0) { + await deleteVariables({ + workspaceSlug, + repositorySlug, + environmentId, + keys: keysToRemove, + authHeader + }); + } + } catch (error) { + throw new SecretSyncError({ error }); + } + }, + + getSecrets: async (secretSync: TBitbucketSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + } +}; diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-schemas.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-schemas.ts new file mode 100644 index 000000000..985d86e8d --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-schemas.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const BitbucketSyncDestinationConfigSchema = z.object({ + repositorySlug: z.string().describe(SecretSyncs.DESTINATION_CONFIG.BITBUCKET.repositorySlug), + environmentId: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.BITBUCKET.environmentId), + workspaceSlug: z.string().describe(SecretSyncs.DESTINATION_CONFIG.BITBUCKET.workspaceSlug) +}); + +const BitbucketSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const BitbucketSyncSchema = BaseSecretSyncSchema(SecretSync.Bitbucket, BitbucketSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Bitbucket), + destinationConfig: BitbucketSyncDestinationConfigSchema +}); + +export const CreateBitbucketSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Bitbucket, + BitbucketSyncOptionsConfig +).extend({ + destinationConfig: BitbucketSyncDestinationConfigSchema +}); + +export const UpdateBitbucketSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Bitbucket, + BitbucketSyncOptionsConfig +).extend({ + destinationConfig: BitbucketSyncDestinationConfigSchema.optional() +}); + +export const BitbucketSyncListItemSchema = z.object({ + name: z.literal("Bitbucket"), + connection: z.literal(AppConnection.Bitbucket), + destination: z.literal(SecretSync.Bitbucket), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-types.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-types.ts new file mode 100644 index 000000000..a28e27487 --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-types.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { TBitbucketConnection } from "@app/services/app-connection/bitbucket"; + +import { BitbucketSyncListItemSchema, BitbucketSyncSchema, CreateBitbucketSyncSchema } from "./bitbucket-sync-schemas"; + +export type TBitbucketSync = z.infer; + +export type TBitbucketSyncInput = z.infer; + +export type TBitbucketSyncListItem = z.infer; + +export type TBitbucketSyncWithCredentials = TBitbucketSync & { + connection: TBitbucketConnection; +}; + +export type TBitbucketVariable = { + key: string; + value?: string; + // Secure variables values are not returned by the API neither are they shown in Bitbucket UI + secured: boolean; + uuid: string; + type: string; +}; + +export type TBitbucketListVariables = { + workspaceSlug: string; + repositorySlug: string; + environmentId?: string; + authHeader: string; +}; + +export type TPutBitbucketVariable = { + authHeader: string; + workspaceSlug: string; + repositorySlug: string; + environmentId?: string; +}; + +export type TDeleteBitbucketVariable = { + authHeader: string; + workspaceSlug: string; + repositorySlug: string; + environmentId?: string; + keys: string[]; +}; + +export type TBitbucketConnectionCredentials = { + authHeader: string; +}; diff --git a/backend/src/services/secret-sync/bitbucket/index.ts b/backend/src/services/secret-sync/bitbucket/index.ts new file mode 100644 index 000000000..d0f20bd45 --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/index.ts @@ -0,0 +1,4 @@ +export * from "./bitbucket-sync-constants"; +export * from "./bitbucket-sync-fns"; +export * from "./bitbucket-sync-schemas"; +export * from "./bitbucket-sync-types"; diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts index 8a9039e2e..9140136a0 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -1,4 +1,6 @@ /* eslint-disable no-await-in-loop */ +import { isAxiosError } from "axios"; + import { request } from "@app/lib/config/request"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; @@ -71,7 +73,7 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr ); }; -const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => { +const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick) => { const { destinationConfig, connection: { @@ -79,15 +81,24 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s } } = secretSync; - await request.delete( - `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`, - { - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json" + try { + await request.delete( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } } + ); + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + // If the secret does not exist, we can ignore this error + return; } - ); + + throw error; + } }; const sleep = async () => @@ -99,6 +110,11 @@ export const RenderSyncFns = { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); for await (const key of Object.keys(secretMap)) { + // If value is empty skip it as render does not allow empty variables + if (secretMap[key].value === "") { + // eslint-disable-next-line no-continue + continue; + } await putEnvironmentSecret(secretSync, secretMap, key); await sleep(); } diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 8d08e4d82..2c02e2ff4 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -25,7 +25,8 @@ export enum SecretSync { Supabase = "supabase", Zabbix = "zabbix", Railway = "railway", - Checkly = "checkly" + Checkly = "checkly", + Bitbucket = "bitbucket" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 3daa9232f..827a41b7d 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -28,6 +28,7 @@ import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password"; import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; +import { BITBUCKET_SYNC_LIST_OPTION, BitbucketSyncFns } from "./bitbucket"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants"; import { ChecklySyncFns } from "./checkly/checkly-sync-fns"; @@ -80,7 +81,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Supabase]: SUPABASE_SYNC_LIST_OPTION, [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION, - [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION + [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION, + [SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -258,6 +260,8 @@ export const SecretSyncFns = { return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Bitbucket: + return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -365,6 +369,9 @@ export const SecretSyncFns = { case SecretSync.Supabase: secretMap = await SupabaseSyncFns.getSecrets(secretSync); break; + case SecretSync.Bitbucket: + secretMap = await BitbucketSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -452,6 +459,8 @@ export const SecretSyncFns = { return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Bitbucket: + return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index a8a017480..1de9838e3 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -28,7 +28,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Supabase]: "Supabase", [SecretSync.Zabbix]: "Zabbix", [SecretSync.Railway]: "Railway", - [SecretSync.Checkly]: "Checkly" + [SecretSync.Checkly]: "Checkly", + [SecretSync.Bitbucket]: "Bitbucket" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -58,7 +59,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Supabase]: AppConnection.Supabase, [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway, - [SecretSync.Checkly]: AppConnection.Checkly + [SecretSync.Checkly]: AppConnection.Checkly, + [SecretSync.Bitbucket]: AppConnection.Bitbucket }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -88,5 +90,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Supabase]: SecretSyncPlanType.Regular, [SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Railway]: SecretSyncPlanType.Regular, - [SecretSync.Checkly]: SecretSyncPlanType.Regular + [SecretSync.Checkly]: SecretSyncPlanType.Regular, + [SecretSync.Bitbucket]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index bd52c0b77..3fdb7fea6 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -74,7 +75,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -110,7 +111,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -153,7 +154,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); @@ -195,7 +196,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); @@ -233,7 +234,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -313,7 +314,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); @@ -429,7 +430,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); @@ -506,7 +507,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); @@ -578,7 +579,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); @@ -644,7 +645,7 @@ export const secretSyncServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId: secretSync.projectId }); diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 2c8753d66..007c79bf7 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,6 +72,12 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { + TBitbucketSync, + TBitbucketSyncInput, + TBitbucketSyncListItem, + TBitbucketSyncWithCredentials +} from "./bitbucket/bitbucket-sync-types"; import { TChecklySync, TChecklySyncInput, @@ -166,7 +172,8 @@ export type TSecretSync = | TZabbixSync | TRailwaySync | TChecklySync - | TSupabaseSync; + | TSupabaseSync + | TBitbucketSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -195,7 +202,8 @@ export type TSecretSyncWithCredentials = | TZabbixSyncWithCredentials | TRailwaySyncWithCredentials | TChecklySyncWithCredentials - | TSupabaseSyncWithCredentials; + | TSupabaseSyncWithCredentials + | TBitbucketSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -224,7 +232,8 @@ export type TSecretSyncInput = | TZabbixSyncInput | TRailwaySyncInput | TChecklySyncInput - | TSupabaseSyncInput; + | TSupabaseSyncInput + | TBitbucketSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -253,7 +262,8 @@ export type TSecretSyncListItem = | TZabbixSyncListItem | TRailwaySyncListItem | TChecklySyncListItem - | TSupabaseSyncListItem; + | TSupabaseSyncListItem + | TBitbucketSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index a4be06b4f..8a08c44dd 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -28,7 +29,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); @@ -59,7 +61,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorId, projectId: tag.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); @@ -76,7 +79,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorId, projectId: tag.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); @@ -93,7 +97,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorId, projectId: tag.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); @@ -109,7 +114,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorId, projectId: tag.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); @@ -122,7 +128,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index b0d114bd8..7aee91273 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -67,6 +67,7 @@ export const getAllSecretReferences = (maybeSecretReference: string) => { export const fnSecretBulkInsert = async ({ // TODO: Pick types here folderId, + commitChanges, orgId, inputSecrets, secretDAL, @@ -134,28 +135,32 @@ export const fnSecretBulkInsert = async ({ tx ); - const commitChanges = secretVersions + const changes = secretVersions .filter(({ type }) => type === SecretType.Shared) .map((sv) => ({ type: CommitType.ADD, secretVersionId: sv.id })); - if (commitChanges.length > 0) { - await folderCommitService.createCommit( - { - actor: { - type: actorType || ActorType.PLATFORM, - metadata: { - id: actor?.actorId - } + if (changes.length > 0) { + if (commitChanges) { + commitChanges.push(...changes); + } else { + await folderCommitService.createCommit( + { + actor: { + type: actorType || ActorType.PLATFORM, + metadata: { + id: actor?.actorId + } + }, + message: "Secret Created", + folderId, + changes }, - message: "Secret Created", - folderId, - changes: commitChanges - }, - tx - ); + tx + ); + } } await secretDAL.upsertSecretReferences( @@ -209,6 +214,7 @@ export const fnSecretBulkUpdate = async ({ tx, inputSecrets, folderId, + commitChanges, orgId, secretDAL, secretVersionDAL, @@ -223,7 +229,10 @@ export const fnSecretBulkUpdate = async ({ const actorType = actor?.type || ActorType.PLATFORM; const sanitizedInputSecrets = inputSecrets.map( - ({ filter, data: { skipMultilineEncoding, type, key, encryptedValue, userId, encryptedComment, metadata } }) => ({ + ({ + filter, + data: { skipMultilineEncoding, type, key, encryptedValue, userId, encryptedComment, metadata, secretMetadata } + }) => ({ filter: { ...filter, folderId }, data: { skipMultilineEncoding, @@ -231,7 +240,7 @@ export const fnSecretBulkUpdate = async ({ key, userId, encryptedComment, - metadata, + metadata: JSON.stringify(metadata || secretMetadata || []), encryptedValue } }) @@ -340,28 +349,32 @@ export const fnSecretBulkUpdate = async ({ { tx } ); - const commitChanges = secretVersions + const changes = secretVersions .filter(({ type }) => type === SecretType.Shared) .map((sv) => ({ type: CommitType.ADD, isUpdate: true, secretVersionId: sv.id })); - if (commitChanges.length > 0) { - await folderCommitService.createCommit( - { - actor: { - type: actorType || ActorType.PLATFORM, - metadata: { - id: actor?.actorId - } + if (changes.length > 0) { + if (commitChanges) { + commitChanges.push(...changes); + } else { + await folderCommitService.createCommit( + { + actor: { + type: actorType || ActorType.PLATFORM, + metadata: { + id: actor?.actorId + } + }, + message: "Secret Updated", + folderId, + changes }, - message: "Secret Updated", - folderId, - changes: commitChanges - }, - tx - ); + tx + ); + } } return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); @@ -377,7 +390,8 @@ export const fnSecretBulkDelete = async ({ secretQueueService, folderCommitService, secretVersionDAL, - projectId + projectId, + commitChanges }: TFnSecretBulkDelete) => { const deletedSecrets = await secretDAL.deleteMany( inputSecrets.map(({ type, secretKey }) => ({ @@ -406,27 +420,31 @@ export const fnSecretBulkDelete = async ({ tx ); - const commitChanges = deletedSecrets + const changes = deletedSecrets .filter(({ type }) => type === SecretType.Shared) .map(({ id }) => ({ type: CommitType.DELETE, secretVersionId: secretVersions[id].id })); - if (commitChanges.length > 0) { - await folderCommitService.createCommit( - { - actor: { - type: actorType || ActorType.PLATFORM, - metadata: { - id: actorId - } + if (changes.length > 0) { + if (commitChanges) { + commitChanges.push(...changes); + } else { + await folderCommitService.createCommit( + { + actor: { + type: actorType || ActorType.PLATFORM, + metadata: { + id: actorId + } + }, + message: "Secret Deleted", + folderId, + changes }, - message: "Secret Deleted", - folderId, - changes: commitChanges - }, - tx - ); + tx + ); + } } return deletedSecrets; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index b77294cae..2fa0ffe9b 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2,7 +2,14 @@ import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; import { Knex } from "knex"; import { z } from "zod"; -import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName, TSecretsV2 } from "@app/db/schemas"; +import { + ActionProjectType, + ProjectMembershipRole, + SecretsV2Schema, + SecretType, + TableName, + TSecretsV2 +} from "@app/db/schemas"; import { hasSecretReadValueOrDescribePermission, throwIfMissingSecretReadValueOrDescribePermission @@ -28,7 +35,7 @@ import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { ActorType } from "../auth/auth-type"; -import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -239,7 +246,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -394,7 +402,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (inputSecret.newSecretName === "") { @@ -620,7 +629,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -756,7 +766,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } @@ -801,7 +812,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -900,7 +912,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -953,7 +966,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -1251,7 +1265,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId: secret.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { @@ -1314,7 +1329,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -1519,14 +1535,17 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, environment, projectId, - secrets: inputSecrets - }: TCreateManySecretDTO) => { + secrets: inputSecrets, + tx: providedTx, + commitChanges + }: TCreateManySecretDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -1603,8 +1622,8 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const newSecrets = await secretDAL.transaction(async (tx) => - fnSecretBulkInsert({ + const executeBulkInsert = async (tx: Knex) => { + return fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1626,6 +1645,7 @@ export const secretV2BridgeServiceFactory = ({ }; }), folderId, + commitChanges, orgId: actorOrgId, secretDAL, resourceMetadataDAL, @@ -1638,8 +1658,13 @@ export const secretV2BridgeServiceFactory = ({ actorId }, tx - }) - ); + }); + }; + + const newSecrets = providedTx + ? await executeBulkInsert(providedTx) + : await secretDAL.transaction(executeBulkInsert); + await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1686,14 +1711,17 @@ export const secretV2BridgeServiceFactory = ({ projectId, secretPath: defaultSecretPath = "/", secrets: inputSecrets, - mode: updateMode - }: TUpdateManySecretDTO) => { + mode: updateMode, + tx: providedTx, + commitChanges + }: TUpdateManySecretDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const secretsToUpdateGroupByPath = groupBy(inputSecrets, (el) => el.secretPath || defaultSecretPath); @@ -1716,18 +1744,20 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const updatedSecrets: Array< - TSecretsV2 & { - secretPath: string; - tags: { - id: string; - slug: string; - color?: string | null; - name: string; - }[]; - } - > = []; - await secretDAL.transaction(async (tx) => { + // Function to execute the bulk update operation + const executeBulkUpdate = async (tx: Knex) => { + const updatedSecrets: Array< + TSecretsV2 & { + secretPath: string; + tags: { + id: string; + slug: string; + color?: string | null; + name: string; + }[]; + } + > = []; + for await (const folder of folders) { if (!folder) throw new NotFoundError({ message: "Folder not found" }); @@ -1846,7 +1876,7 @@ export const secretV2BridgeServiceFactory = ({ { operator: "eq", field: `${TableName.SecretV2}.key` as "key", - value: el.secretKey + value: el.newSecretName as string }, { operator: "eq", @@ -1900,6 +1930,7 @@ export const secretV2BridgeServiceFactory = ({ orgId: actorOrgId, folderCommitService, tx, + commitChanges, inputSecrets: secretsToUpdate.map((el) => { const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; const encryptedValue = @@ -1977,7 +2008,13 @@ export const secretV2BridgeServiceFactory = ({ updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } - }); + + return updatedSecrets; + }; + + const updatedSecrets = providedTx + ? await executeBulkUpdate(providedTx) + : await secretDAL.transaction(executeBulkUpdate); await secretDAL.invalidateSecretCacheByProjectId(projectId); await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined))); @@ -2034,14 +2071,17 @@ export const secretV2BridgeServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId - }: TDeleteManySecretDTO) => { + actorOrgId, + tx: providedTx, + commitChanges + }: TDeleteManySecretDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -2094,24 +2134,29 @@ export const secretV2BridgeServiceFactory = ({ ); }); + const executeBulkDelete = async (tx: Knex) => { + return fnSecretBulkDelete({ + secretDAL, + secretQueueService, + folderCommitService, + secretVersionDAL, + inputSecrets: inputSecrets.map(({ type, secretKey }) => ({ + secretKey, + type: type || SecretType.Shared + })), + projectId, + folderId, + actorId, + actorType: actor, + commitChanges, + tx + }); + }; + try { - const secretsDeleted = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ - secretDAL, - secretQueueService, - folderCommitService, - secretVersionDAL, - inputSecrets: inputSecrets.map(({ type, secretKey }) => ({ - secretKey, - type: type || SecretType.Shared - })), - projectId, - folderId, - actorId, - actorType: actor, - tx - }) - ); + const secretsDeleted = providedTx + ? await executeBulkDelete(providedTx) + : await secretDAL.transaction(executeBulkDelete); await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); @@ -2197,7 +2242,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId: folder.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const canRead = @@ -2262,7 +2308,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -2309,7 +2356,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); @@ -2693,7 +2741,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { @@ -2786,7 +2835,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, @@ -2910,7 +2960,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const canRead = diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 4027f784c..49d0eaf37 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -8,7 +8,7 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; -import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; @@ -167,6 +167,7 @@ export type TFnSecretBulkInsert = { folderId: string; orgId: string; tx?: Knex; + commitChanges?: TCommitResourceChangeDTO[]; inputSecrets: Array< Omit & { tagIds?: string[]; @@ -214,6 +215,7 @@ export type TFnSecretBulkUpdate = { actorId?: string; }; tx?: Knex; + commitChanges?: TCommitResourceChangeDTO[]; }; export type TFnSecretBulkDelete = { @@ -223,6 +225,7 @@ export type TFnSecretBulkDelete = { actorId: string; actorType?: string; tx?: Knex; + commitChanges?: TCommitResourceChangeDTO[]; secretDAL: Pick; secretQueueService: { removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 1442607cb..3b97f5891 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -3,6 +3,7 @@ import path from "path"; import RE2 from "re2"; import { + ActionProjectType, SecretEncryptionAlgo, SecretKeyEncoding, SecretType, @@ -180,7 +181,8 @@ export const recursivelyGetSecretPaths = ({ actorId: auth.actorId, projectId, actorAuthMethod: auth.actorAuthMethod, - actorOrgId: auth.actorOrgId + actorOrgId: auth.actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); // Filter out paths that the user does not have permission to access, and paths that are not in the current path diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index a7d3b2aea..7df668cb4 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -3,6 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { + ActionProjectType, ProjectMembershipRole, ProjectUpgradeStatus, ProjectVersion, @@ -215,7 +216,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -332,7 +334,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -492,7 +495,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -608,7 +612,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); let paths: { folderId: string; path: string }[] = []; @@ -713,7 +718,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, @@ -818,7 +824,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.Create, @@ -904,7 +911,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1026,7 +1034,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.Delete, @@ -2586,7 +2595,8 @@ export const secretServiceFactory = ({ actorId, projectId: folder.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); const secretVersions = await secretVersionDAL.findBySecretId(secretId, { @@ -2678,7 +2688,8 @@ export const secretServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -2783,7 +2794,8 @@ export const secretServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -2889,7 +2901,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -2974,7 +2987,8 @@ export const secretServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const { botKey } = await projectBotService.getBotKey(project.id); @@ -3381,7 +3395,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -3409,7 +3424,8 @@ export const secretServiceFactory = ({ actorId: actor.id, projectId: params.projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager }); const secrets = secretV2BridgeService.getSecretsByFolderMappings({ ...params, userId: actor.id }, permission); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 8af15818e..67766cb00 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -547,3 +547,33 @@ export enum SecretProtectionType { } export type TStartSecretsV2MigrationDTO = TProjectPermission; + +export type TProcessNewCommitRawDTO = { + secrets: { + create?: { + secretKey: string; + secretValue: string; + secretComment?: string; + skipMultilineEncoding?: boolean; + tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; + metadata?: { source?: string }; + }[]; + update?: { + secretKey: string; + newSecretName?: string; + secretValue?: string; + secretComment?: string; + skipMultilineEncoding?: boolean; + tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; + metadata?: { source?: string }; + }[]; + delete?: { secretKey: string }[]; + }; + folders: { + create?: { folderName: string; description?: string }[]; + update?: { folderName: string; description?: string | null; id: string }[]; + delete?: { folderName: string; id: string }[]; + }; +}; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 07362ff65..6d61cf898 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, @@ -65,7 +66,8 @@ export const serviceTokenServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); @@ -120,7 +122,8 @@ export const serviceTokenServiceFactory = ({ actorId, projectId: serviceToken.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); @@ -154,7 +157,8 @@ export const serviceTokenServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index bee414179..a111d5372 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -50,7 +50,7 @@ const buildSlackPayload = (notification: TNotification) => { *Secret path*: ${payload.secretPath || "/"} *Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")} -View the complete details <${appCfg.SITE_URL}/projects/${payload.projectId}/secret-manager/approval?requestId=${ +View the complete details <${appCfg.SITE_URL}/projects/secret-management/${payload.projectId}/approval?requestId=${ payload.requestId }|here>.`; diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index ba3f2170a..eb58ee5bd 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { TWebhooksInsert } from "@app/db/schemas"; +import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; @@ -54,7 +54,8 @@ export const webhookServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); @@ -92,7 +93,8 @@ export const webhookServiceFactory = ({ actorId, projectId: webhook.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); @@ -109,7 +111,8 @@ export const webhookServiceFactory = ({ actorId, projectId: webhook.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); @@ -126,7 +129,8 @@ export const webhookServiceFactory = ({ actorId, projectId: webhook.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); const project = await projectDAL.findById(webhook.projectId); @@ -177,7 +181,8 @@ export const webhookServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); diff --git a/docs/api-reference/endpoints/app-connections/okta/available.mdx b/docs/api-reference/endpoints/app-connections/okta/available.mdx new file mode 100644 index 000000000..169ddb51b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/okta/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/create.mdx b/docs/api-reference/endpoints/app-connections/okta/create.mdx new file mode 100644 index 000000000..732f83fa8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/okta" +--- + + + Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/okta/delete.mdx b/docs/api-reference/endpoints/app-connections/okta/delete.mdx new file mode 100644 index 000000000..09abf8549 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/okta/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/okta/get-by-id.mdx new file mode 100644 index 000000000..789f7b87d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/okta/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/okta/get-by-name.mdx new file mode 100644 index 000000000..763d42d72 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/okta/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/list.mdx b/docs/api-reference/endpoints/app-connections/okta/list.mdx new file mode 100644 index 000000000..81ac560f2 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/okta" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/update.mdx b/docs/api-reference/endpoints/app-connections/okta/update.mdx new file mode 100644 index 000000000..b063eeade --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/okta/{connectionId}" +--- + + + Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/create.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/create.mdx new file mode 100644 index 000000000..a92c1bc10 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/okta-client-secret" +--- + + + Check out the configuration docs for [Okta Client Secret Rotations](/documentation/platform/secret-rotation/okta-client-secret) to learn how to obtain the required parameters. + diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/delete.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/delete.mdx new file mode 100644 index 000000000..598eb1559 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/okta-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id.mdx new file mode 100644 index 000000000..b2c9c281e --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name.mdx new file mode 100644 index 000000000..0eb400b7d --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/okta-client-secret/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..a74c52d92 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/list.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/list.mdx new file mode 100644 index 000000000..bb8b6777f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/okta-client-secret" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets.mdx new file mode 100644 index 000000000..71f7f2fbf --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/okta-client-secret/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/update.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/update.mdx new file mode 100644 index 000000000..3cae3f895 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/okta-client-secret/{rotationId}" +--- + + + Check out the configuration docs for [Okta Client Secret Rotations](/documentation/platform/secret-rotation/okta-client-secret) to learn how to obtain the required parameters. + diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/create.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/create.mdx new file mode 100644 index 000000000..69535ace0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/bitbucket" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/delete.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/delete.mdx new file mode 100644 index 000000000..55cfc0359 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/bitbucket/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-id.mdx new file mode 100644 index 000000000..46373e310 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/bitbucket/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-name.mdx new file mode 100644 index 000000000..82bb47d45 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/bitbucket/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/import-secrets.mdx new file mode 100644 index 000000000..eed3d9124 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/bitbucket/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/list.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/list.mdx new file mode 100644 index 000000000..98bf3fdda --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/bitbucket" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/remove-secrets.mdx new file mode 100644 index 000000000..3d52e14e2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/bitbucket/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/sync-secrets.mdx new file mode 100644 index 000000000..47fee7642 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/bitbucket/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/update.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/update.mdx new file mode 100644 index 000000000..c9dfac1c8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/bitbucket/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index a32453c89..e8f6745c3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -78,7 +78,10 @@ }, { "group": "Infisical SSH", - "pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"] + "pages": [ + "documentation/platform/ssh/overview", + "documentation/platform/ssh/host-groups" + ] }, { "group": "Key Management (KMS)", @@ -146,6 +149,7 @@ "documentation/platform/secret-rotation/ldap-password", "documentation/platform/secret-rotation/mssql-credentials", "documentation/platform/secret-rotation/mysql-credentials", + "documentation/platform/secret-rotation/okta-client-secret", "documentation/platform/secret-rotation/oracledb-credentials", "documentation/platform/secret-rotation/postgres-credentials" ] @@ -375,7 +379,10 @@ }, { "group": "Architecture", - "pages": ["internals/architecture/components", "internals/architecture/cloud"] + "pages": [ + "internals/architecture/components", + "internals/architecture/cloud" + ] }, "internals/security", "internals/service-tokens" @@ -481,6 +488,7 @@ "integrations/app-connections/mssql", "integrations/app-connections/mysql", "integrations/app-connections/oci", + "integrations/app-connections/okta", "integrations/app-connections/oracledb", "integrations/app-connections/postgres", "integrations/app-connections/railway", @@ -508,6 +516,7 @@ "integrations/secret-syncs/azure-app-configuration", "integrations/secret-syncs/azure-devops", "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/bitbucket", "integrations/secret-syncs/camunda", "integrations/secret-syncs/checkly", "integrations/secret-syncs/cloudflare-pages", @@ -551,7 +560,10 @@ "integrations/cloud/gcp-secret-manager", { "group": "Cloudflare", - "pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"] + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] }, "integrations/cloud/terraform-cloud", "integrations/cloud/databricks", @@ -663,7 +675,11 @@ "cli/commands/reset", { "group": "infisical scan", - "pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"] + "pages": [ + "cli/commands/scan", + "cli/commands/scan-git-changes", + "cli/commands/scan-install" + ] } ] }, @@ -987,7 +1003,9 @@ "pages": [ { "group": "Kubernetes", - "pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"] + "pages": [ + "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" + ] }, "api-reference/endpoints/dynamic-secrets/create", "api-reference/endpoints/dynamic-secrets/update", @@ -1093,6 +1111,19 @@ "api-reference/endpoints/secret-rotations/mysql-credentials/update" ] }, + { + "group": "Okta Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/okta-client-secret/create", + "api-reference/endpoints/secret-rotations/okta-client-secret/delete", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/okta-client-secret/list", + "api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/okta-client-secret/update" + ] + }, { "group": "OracleDB Credentials", "pages": [ @@ -1496,6 +1527,18 @@ "api-reference/endpoints/app-connections/oci/delete" ] }, + { + "group": "Okta", + "pages": [ + "api-reference/endpoints/app-connections/okta/list", + "api-reference/endpoints/app-connections/okta/available", + "api-reference/endpoints/app-connections/okta/get-by-id", + "api-reference/endpoints/app-connections/okta/get-by-name", + "api-reference/endpoints/app-connections/okta/create", + "api-reference/endpoints/app-connections/okta/update", + "api-reference/endpoints/app-connections/okta/delete" + ] + }, { "group": "OracleDB", "pages": [ @@ -1707,6 +1750,20 @@ "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" ] }, + { + "group": "Bitbucket", + "pages": [ + "api-reference/endpoints/secret-syncs/bitbucket/list", + "api-reference/endpoints/secret-syncs/bitbucket/get-by-id", + "api-reference/endpoints/secret-syncs/bitbucket/get-by-name", + "api-reference/endpoints/secret-syncs/bitbucket/create", + "api-reference/endpoints/secret-syncs/bitbucket/update", + "api-reference/endpoints/secret-syncs/bitbucket/delete", + "api-reference/endpoints/secret-syncs/bitbucket/sync-secrets", + "api-reference/endpoints/secret-syncs/bitbucket/import-secrets", + "api-reference/endpoints/secret-syncs/bitbucket/remove-secrets" + ] + }, { "group": "Camunda", "pages": [ @@ -2228,6 +2285,7 @@ "sdks/languages/java", "sdks/languages/csharp", "sdks/languages/cpp", + "sdks/languages/rust", "sdks/languages/go", "sdks/languages/ruby" ] diff --git a/docs/documentation/getting-started/sdks.mdx b/docs/documentation/getting-started/sdks.mdx index b3e8a3925..e91ff1906 100644 --- a/docs/documentation/getting-started/sdks.mdx +++ b/docs/documentation/getting-started/sdks.mdx @@ -13,9 +13,12 @@ Prerequisites: Follow the instructions for your language use the SDK for it: -- [Node SDK](https://infisical.com/docs/sdks/languages/node) +- [Node.js SDK](https://infisical.com/docs/sdks/languages/node) - [Python SDK](https://infisical.com/docs/sdks/languages/python) - [Java SDK](https://infisical.com/docs/sdks/languages/java) - [.NET SDK](https://infisical.com/docs/sdks/languages/csharp) +- [Go SDK](https://infisical.com/docs/sdks/languages/go) +- [C++ SDK](https://infisical.com/docs/sdks/languages/cpp) +- [Ruby SDK](https://infisical.com/docs/sdks/languages/ruby) Missing a language? [Throw in a request here](https://github.com/Infisical/infisical/issues). diff --git a/docs/documentation/platform/secret-rotation/okta-client-secret.mdx b/docs/documentation/platform/secret-rotation/okta-client-secret.mdx new file mode 100644 index 000000000..d1f4b159e --- /dev/null +++ b/docs/documentation/platform/secret-rotation/okta-client-secret.mdx @@ -0,0 +1,145 @@ +--- +title: "Okta Client Secret" +description: "Learn how to automatically rotate Okta Client Secrets." +--- + +## Prerequisites + +- Create an [Okta Connection](/integrations/app-connections/okta). + +## Create an Okta Client Secret Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **Okta Client Secret** option. + + ![Select Okta Client Secret](/images/secret-rotations-v2/okta-client-secret/select-okta.png) + + 3. Configure the rotation behavior, then click **Next**. + + ![Rotation Configuration](/images/secret-rotations-v2/okta-client-secret/configuration.png) + + - **Okta Connection** - the connection that will perform the rotation of the specified application's Client Secret. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Select the Okta application whose Client Secret you want to rotate. Then click **Next**. + + ![Rotation Parameters](/images/secret-rotations-v2/okta-client-secret/parameters.png) + + 5. Specify the secret names that the client credentials should be mapped to. Then click **Next**. + + ![Rotation Secrets Mapping](/images/secret-rotations-v2/okta-client-secret/mappings.png) + + - **Client ID** - the name of the secret that the application Client ID will be mapped to. + - **Client Secret** - the name of the secret that the rotated Client Secret will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + + ![Rotation Details](/images/secret-rotations-v2/okta-client-secret/details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + + ![Rotation Review](/images/secret-rotations-v2/okta-client-secret/review.png) + + 8. Your **Okta Client Secret** credentials are now available for use via the mapped secrets. + + ![Rotation Created](/images/secret-rotations-v2/okta-client-secret/created.png) + + + To create an Okta Client Secret Rotation, make an API request to the [Create Okta Client Secret Rotation](/api-reference/endpoints/secret-rotations/okta-client-secret/create) API endpoint. + + You will first need the **Client ID** of the Okta application you want to rotate the secret for. This can be obtained from the applications dashboard. + + ![Okta Client ID](/images/secret-rotations-v2/okta-client-secret/client-id.png) + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/okta-client-secret \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-okta-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my client secret rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "clientId": "...", + }, + "secretsMapping": { + "clientId": "OKTA_CLIENT_ID", + "clientSecret": "OKTA_CLIENT_SECRET" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-okta-rotation", + "description": "my client secret rotation", + "secretsMapping": { + "clientId": "OKTA_CLIENT_ID", + "clientSecret": "OKTA_CLIENT_SECRET" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "okta", + "name": "my-okta-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "okta-client-secret", + "parameters": { + "clientId": "..." + } + } + } + ``` + + diff --git a/docs/images/app-connections/bitbucket/step-4-secret-sync.png b/docs/images/app-connections/bitbucket/step-4-secret-sync.png new file mode 100644 index 000000000..5f8f81534 Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-4-secret-sync.png differ diff --git a/docs/images/app-connections/okta/step-1.png b/docs/images/app-connections/okta/step-1.png new file mode 100644 index 000000000..478e87705 Binary files /dev/null and b/docs/images/app-connections/okta/step-1.png differ diff --git a/docs/images/app-connections/okta/step-2.png b/docs/images/app-connections/okta/step-2.png new file mode 100644 index 000000000..69645bf84 Binary files /dev/null and b/docs/images/app-connections/okta/step-2.png differ diff --git a/docs/images/app-connections/okta/step-3.png b/docs/images/app-connections/okta/step-3.png new file mode 100644 index 000000000..de8d69a24 Binary files /dev/null and b/docs/images/app-connections/okta/step-3.png differ diff --git a/docs/images/app-connections/okta/step-4.png b/docs/images/app-connections/okta/step-4.png new file mode 100644 index 000000000..eb6426a37 Binary files /dev/null and b/docs/images/app-connections/okta/step-4.png differ diff --git a/docs/images/app-connections/okta/step-5.png b/docs/images/app-connections/okta/step-5.png new file mode 100644 index 000000000..54bf24bde Binary files /dev/null and b/docs/images/app-connections/okta/step-5.png differ diff --git a/docs/images/sdks/languages/cpp.svg b/docs/images/sdks/languages/cpp.svg new file mode 100644 index 000000000..caced490f --- /dev/null +++ b/docs/images/sdks/languages/cpp.svg @@ -0,0 +1,11 @@ + + + C++ + + + + + + + + diff --git a/docs/images/sdks/languages/dotnet.svg b/docs/images/sdks/languages/dotnet.svg new file mode 100644 index 000000000..c864d45d2 --- /dev/null +++ b/docs/images/sdks/languages/dotnet.svg @@ -0,0 +1,8 @@ + + + .NET + + + + + diff --git a/docs/images/sdks/languages/go.svg b/docs/images/sdks/languages/go.svg new file mode 100644 index 000000000..ec964ffc3 --- /dev/null +++ b/docs/images/sdks/languages/go.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/images/sdks/languages/java.svg b/docs/images/sdks/languages/java.svg new file mode 100644 index 000000000..228b75f6b --- /dev/null +++ b/docs/images/sdks/languages/java.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/images/sdks/languages/node.svg b/docs/images/sdks/languages/node.svg new file mode 100644 index 000000000..abf449bce --- /dev/null +++ b/docs/images/sdks/languages/node.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/images/sdks/languages/python.svg b/docs/images/sdks/languages/python.svg new file mode 100644 index 000000000..cfbb36f36 --- /dev/null +++ b/docs/images/sdks/languages/python.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/images/sdks/languages/ruby.svg b/docs/images/sdks/languages/ruby.svg new file mode 100644 index 000000000..eaae0bdbc --- /dev/null +++ b/docs/images/sdks/languages/ruby.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/sdks/languages/rust.svg b/docs/images/sdks/languages/rust.svg new file mode 100644 index 000000000..1410406a1 --- /dev/null +++ b/docs/images/sdks/languages/rust.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/images/secret-rotations-v2/okta-client-secret/client-id.png b/docs/images/secret-rotations-v2/okta-client-secret/client-id.png new file mode 100644 index 000000000..83ac00c4f Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/client-id.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/configuration.png b/docs/images/secret-rotations-v2/okta-client-secret/configuration.png new file mode 100644 index 000000000..fe511bbcc Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/configuration.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/created.png b/docs/images/secret-rotations-v2/okta-client-secret/created.png new file mode 100644 index 000000000..4e0c8a5da Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/created.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/details.png b/docs/images/secret-rotations-v2/okta-client-secret/details.png new file mode 100644 index 000000000..6eafb89bf Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/details.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/mappings.png b/docs/images/secret-rotations-v2/okta-client-secret/mappings.png new file mode 100644 index 000000000..baeaa1605 Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/mappings.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/parameters.png b/docs/images/secret-rotations-v2/okta-client-secret/parameters.png new file mode 100644 index 000000000..b5a6a716b Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/parameters.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/review.png b/docs/images/secret-rotations-v2/okta-client-secret/review.png new file mode 100644 index 000000000..45462ceb5 Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/review.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/select-okta.png b/docs/images/secret-rotations-v2/okta-client-secret/select-okta.png new file mode 100644 index 000000000..34347245f Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/select-okta.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-destination.png b/docs/images/secret-syncs/bitbucket/configure-destination.png new file mode 100644 index 000000000..f393387be Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-destination.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-details.png b/docs/images/secret-syncs/bitbucket/configure-details.png new file mode 100644 index 000000000..64f9c2720 Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-details.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-source.png b/docs/images/secret-syncs/bitbucket/configure-source.png new file mode 100644 index 000000000..2b70abba5 Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-source.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-sync-options.png b/docs/images/secret-syncs/bitbucket/configure-sync-options.png new file mode 100644 index 000000000..471b2851c Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/bitbucket/review-configuration.png b/docs/images/secret-syncs/bitbucket/review-configuration.png new file mode 100644 index 000000000..e76c726fa Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/review-configuration.png differ diff --git a/docs/images/secret-syncs/bitbucket/select-option.png b/docs/images/secret-syncs/bitbucket/select-option.png new file mode 100644 index 000000000..85446dd55 Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/select-option.png differ diff --git a/docs/images/secret-syncs/bitbucket/sync-created.png b/docs/images/secret-syncs/bitbucket/sync-created.png new file mode 100644 index 000000000..31afde5be Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/sync-created.png differ diff --git a/docs/integrations/app-connections/bitbucket.mdx b/docs/integrations/app-connections/bitbucket.mdx index e4f9ae29f..be4fdbee7 100644 --- a/docs/integrations/app-connections/bitbucket.mdx +++ b/docs/integrations/app-connections/bitbucket.mdx @@ -47,6 +47,19 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck ![Configure Permissions](/images/app-connections/bitbucket/step-4.png) + + ``` + read:workspace:bitbucket + admin:workspace:bitbucket + read:user:bitbucket + read:repository:bitbucket + read:pipeline:bitbucket + write:pipeline:bitbucket + admin:pipeline:bitbucket + ``` + + ![Configure Permissions](/images/app-connections/bitbucket/step-4-secret-sync.png) + Click **Next**. diff --git a/docs/integrations/app-connections/okta.mdx b/docs/integrations/app-connections/okta.mdx new file mode 100644 index 000000000..3c1295cf8 --- /dev/null +++ b/docs/integrations/app-connections/okta.mdx @@ -0,0 +1,99 @@ +--- +title: "Okta Connection" +description: "Learn how to configure an Okta Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guides/create-an-api-token/main/) to connect with Okta. + +## Create Okta API Token + + + + From the Okta admin dashboard, navigate to **Security > API > Tokens** and click **Create token**. + + ![Create API Token](/images/app-connections/okta/step-1.png) + + + Enter the token name and select **Any IP** for the second dropdown, then click **Create token**. + + ![Provide Info](/images/app-connections/okta/step-2.png) + + + Copy the token from the modal for later steps. + + ![Copy Token](/images/app-connections/okta/step-3.png) + + + +## Create Okta Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **Add Connection** button and select **Okta** from the list of available connections. + + + Complete the Okta Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - Your Okta instance URL + - The API Token from earlier steps + + ![Connection Modal](/images/app-connections/okta/step-4.png) + + + After clicking Create, your **Okta Connection** is established and ready to use with your Infisical projects. + + ![Connection Created](/images/app-connections/okta/step-5.png) + + + + + To create a Okta Connection, make an API request to the [Create Okta Connection](/api-reference/endpoints/app-connections/okta/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/okta \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-okta-connection", + "method": "api-token", + "credentials": { + "instanceUrl": "https://example.okta.com", + "apiToken": "" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-okta-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "okta", + "method": "api-token", + "credentials": { + "instanceUrl": "https://example.okta.com" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/bitbucket.mdx b/docs/integrations/secret-syncs/bitbucket.mdx new file mode 100644 index 000000000..9793e89c9 --- /dev/null +++ b/docs/integrations/secret-syncs/bitbucket.mdx @@ -0,0 +1,159 @@ +--- +title: "Bitbucket Sync" +description: "Learn how to configure a Bitbucket Sync for Infisical." +--- + +**Prerequisites:** +- Create a [Bitbucket Connection](/integrations/app-connections/bitbucket) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Bitbucket](/images/secret-syncs/bitbucket/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/bitbucket/configure-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/bitbucket/configure-destination.png) + + - **Bitbucket Connection**: The Bitbucket Connection to authenticate with. + - **Workspace**: The Bitbucket workspace to sync secrets to. + - **Repository**: The Bitbucket repository to sync secrets to. + - **Deployment Environment (Optional)**: The Bitbucket deployment environment to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Sync Options](/images/secret-syncs/bitbucket/configure-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Bitbucket does not support importing secrets. + + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Bitbucket Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/bitbucket/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Bitbucket Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/bitbucket/review-configuration.png) + + + If enabled, your Bitbucket Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/bitbucket/sync-created.png) + + + + + To create a **Bitbucket Sync**, make an API request to the [Create Bitbucket Sync](/api-reference/endpoints/secret-syncs/bitbucket/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/bitbucket \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-bitbucket-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "workspaceSlug": "my-bitbucket-workspace", + "repositorySlug": "my-bitbucket-repository" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-bitbucket-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "bitbucket", + "name": "my-bitbucket-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "bitbucket", + "destinationConfig": { + "workspaceSlug": "my-bitbucket-workspace", + "repositorySlug": "my-bitbucket-repository" + } + } + } + ``` + + diff --git a/docs/sdks/languages/cpp.mdx b/docs/sdks/languages/cpp.mdx index c75032ab1..2e22def9c 100644 --- a/docs/sdks/languages/cpp.mdx +++ b/docs/sdks/languages/cpp.mdx @@ -2,5 +2,5 @@ title: "Infisical C++ SDK" sidebarTitle: "C++" url: "https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk" -icon: "c" +icon: "/images/sdks/languages/cpp.svg" --- \ No newline at end of file diff --git a/docs/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx index 71ac7337e..524778a4a 100644 --- a/docs/sdks/languages/csharp.mdx +++ b/docs/sdks/languages/csharp.mdx @@ -2,7 +2,7 @@ title: "Infisical .NET SDK" sidebarTitle: ".NET" url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" -icon: "bars" +icon: "/images/sdks/languages/dotnet.svg" --- {/* If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application. diff --git a/docs/sdks/languages/go.mdx b/docs/sdks/languages/go.mdx index b12b442a8..312f7e307 100644 --- a/docs/sdks/languages/go.mdx +++ b/docs/sdks/languages/go.mdx @@ -1,7 +1,7 @@ --- title: "Infisical Go SDK" sidebarTitle: "Go" -icon: "golang" +icon: "/images/sdks/languages/go.svg" --- If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application. diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx index 8b8ade7b0..359afb987 100644 --- a/docs/sdks/languages/java.mdx +++ b/docs/sdks/languages/java.mdx @@ -2,7 +2,7 @@ title: "Infisical Java SDK" sidebarTitle: "Java" url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk" -icon: "java" +icon: "/images/sdks/languages/java.svg" --- { diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx index c947cb82d..69bb36e97 100644 --- a/docs/sdks/languages/node.mdx +++ b/docs/sdks/languages/node.mdx @@ -2,7 +2,7 @@ title: "Infisical Node.js SDK" sidebarTitle: "Node.js" url: "https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk" -icon: "node" +icon: "/images/sdks/languages/node.svg" --- {/* diff --git a/docs/sdks/languages/python.mdx b/docs/sdks/languages/python.mdx index f7a2ee90b..f888c0819 100644 --- a/docs/sdks/languages/python.mdx +++ b/docs/sdks/languages/python.mdx @@ -2,7 +2,7 @@ title: "Infisical Python SDK" sidebarTitle: "Python" url: "https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk" -icon: "python" +icon: "/images/sdks/languages/python.svg" --- {/* If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application. diff --git a/docs/sdks/languages/ruby.mdx b/docs/sdks/languages/ruby.mdx index b6fe0863a..5780c48ec 100644 --- a/docs/sdks/languages/ruby.mdx +++ b/docs/sdks/languages/ruby.mdx @@ -1,7 +1,7 @@ --- title: "Infisical Ruby SDK" sidebarTitle: "Ruby" -icon: "diamond" +icon: "/images/sdks/languages/ruby.svg" --- diff --git a/docs/sdks/languages/rust.mdx b/docs/sdks/languages/rust.mdx new file mode 100644 index 000000000..3d2f16175 --- /dev/null +++ b/docs/sdks/languages/rust.mdx @@ -0,0 +1,6 @@ +--- +title: "Infisical Rust SDK" +sidebarTitle: "Rust" +icon: "/images/sdks/languages/rust.svg" +url: "https://github.com/Infisical/rust-sdk?tab=readme-ov-file#infisical--the-official-infisical-rust-sdk" +--- \ No newline at end of file diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index 3d91713da..32192805d 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -3,6 +3,8 @@ title: "SDKs" sidebarTitle: "Introduction" --- + + From local development to production, Infisical SDKs provide the easiest way for your app to fetch back secrets from Infisical on demand. - Install and initialize a language-specific client SDK into your application @@ -10,29 +12,36 @@ From local development to production, Infisical SDKs provide the easiest way for - Fetch secrets on demand - + Manage secrets for your Node application on demand - + Manage secrets for your Python application on demand - + Manage secrets for your Java application on demand - - Manage secrets for your Go application on demand - - + Manage secrets for your .NET application on demand - + Manage secrets for your C++ application on demand - + + Manage secrets for your Rust application on demand + + + Manage secrets for your Go application on demand + + Manage secrets for your Ruby application on demand + + We're always looking for new languages to support. If you'd like to see a new language added, please let us know by opening an issue on our [GitHub repository](https://github.com/Infisical/infisical/issues). + + ## FAQ diff --git a/frontend/public/images/integrations/Okta.png b/frontend/public/images/integrations/Okta.png new file mode 100644 index 000000000..d742d4347 Binary files /dev/null and b/frontend/public/images/integrations/Okta.png differ diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 326dbc10e..1283c02d1 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -6,7 +6,6 @@ import { twMerge } from "tailwind-merge"; import { useOrganization, useWorkspace } from "@app/context"; import { useToggle } from "@app/hooks"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { createNotification } from "../notifications"; import { IconButton, Select, SelectItem, Tooltip } from "../v2"; @@ -93,7 +92,7 @@ export default function NavHeader({ {pageName === "Secrets" ? ( @@ -129,7 +128,7 @@ export default function NavHeader({
@@ -191,7 +190,7 @@ export default function NavHeader({
) : ( ) : ( - ( - - - - )} - />
; +const PROJECT_TYPE_MENU_ITEMS = [ + { + label: "Secrets Management", + value: ProjectType.SecretManager + }, + { + label: "Cert Management", + value: ProjectType.CertificateManager + }, + { + label: "KMS", + value: ProjectType.KMS + }, + { + label: "SSH", + value: ProjectType.SSH + }, + { + label: "Secret Scanning", + value: ProjectType.SecretScanning + } +]; + const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); @@ -70,18 +97,11 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { OrgPermissionSubjects.ProjectTemplates ); - const { data: projectTemplates = [] } = useListProjectTemplates({ - enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates) - }); - - const { data: externalKmsList } = useGetExternalKmsList(currentOrg.id, { - enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms) - }); - const { control, handleSubmit, reset, + watch, formState: { isSubmitting, errors } } = useForm({ resolver: zodResolver(formSchema), @@ -91,6 +111,16 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { } }); + const selectedProjectType = watch("type"); + const { data: projectTemplates = [] } = useListProjectTemplates({ + enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates), + select: (template) => template.filter((el) => el.type === selectedProjectType) + }); + + const { data: externalKmsList } = useGetExternalKmsList(currentOrg.id, { + enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms) + }); + useEffect(() => { if (Object.keys(errors).length > 0) { console.log("Current form errors:", errors); @@ -101,7 +131,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { name, description, kmsKeyId, - template + template, + type }: TAddProjectFormData) => { // type check if (!currentOrg) return; @@ -113,7 +144,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { projectName: name, projectDescription: description, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - template + template, + type }); await refetchWorkspaces(); @@ -121,7 +153,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { reset(); onOpenChange(false); navigate({ - to: getProjectHomePage(project.defaultProduct), + to: getProjectHomePage(project.type), params: { projectId: project.id } }); } catch (err) { @@ -150,6 +182,42 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { )} /> + ( + +
+ {PROJECT_TYPE_MENU_ITEMS.map((el) => ( +
field.onChange(el.value)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") { + field.onChange(el.value); + } + }} + > + +
{el.label}
+
+ ))} +
+
+ )} + /> { )} /> + { + const inactiveIndex = activeIndex === 0 ? 1 : 0; + + const activeCredentials = generatedCredentials[activeIndex]; + const inactiveCredentials = generatedCredentials[inactiveIndex]; + + return ( + + {activeCredentials?.clientId} + + {activeCredentials?.clientSecret} + + + } + inactiveCredentials={ + <> + {inactiveCredentials?.clientId} + + {inactiveCredentials?.clientSecret} + + + } + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index c81eb9920..33d3fccc1 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -22,6 +22,7 @@ import { import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared"; import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials"; +import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials"; type Props = { secretRotation?: TSecretRotationV2; @@ -99,6 +100,13 @@ const Content = ({ secretRotation }: ContentProps) => { /> ); break; + case SecretRotation.OktaClientSecret: + Component = ( + + ); + break; default: throw new Error("Unhandled View Generated Credential Rotation Type"); } diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/OktaClientSecretRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/OktaClientSecretRotationParametersFields.tsx new file mode 100644 index 000000000..bb306615d --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/OktaClientSecretRotationParametersFields.tsx @@ -0,0 +1,51 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { useOktaConnectionListApps } from "@app/hooks/api/appConnections/okta"; +import { TOktaApp } from "@app/hooks/api/appConnections/okta/types"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const OktaClientSecretRotationParametersFields = () => { + const { control, watch, setValue } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.OktaClientSecret; + } + >(); + + const connectionId = watch("connection.id"); + + const { data: apps, isPending: isAppsPending } = useOktaConnectionListApps(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + ( + + app.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("parameters.clientId", (option as SingleValue)?.id ?? ""); + }} + options={apps} + placeholder="Select an application..." + getOptionLabel={(option) => option.label} + getOptionValue={(option) => option.id} + /> + + )} + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx index 959ca2d9e..3f489b04e 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx @@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRo import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields"; import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields"; import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields"; +import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields"; import { SqlCredentialsRotationParametersFields } from "./shared"; const COMPONENT_MAP: Record = { @@ -17,7 +18,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields, [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, - [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields, + [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/OktaClientSecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/OktaClientSecretRotationReviewFields.tsx new file mode 100644 index 000000000..a9fc4068e --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/OktaClientSecretRotationReviewFields.tsx @@ -0,0 +1,29 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +import { SecretRotationReviewSection } from "./shared"; + +export const OktaClientSecretRotationReviewFields = () => { + const { watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.OktaClientSecret; + } + >(); + + const [parameters, { clientId, clientSecret }] = watch(["parameters", "secretsMapping"]); + + return ( + <> + + {parameters.clientId} + + + {clientId} + {clientSecret} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index 17cc34f27..636cc98cc 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -10,6 +10,7 @@ import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotati import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields"; import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields"; import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields"; +import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields"; import { SqlCredentialsRotationReviewFields } from "./shared"; const COMPONENT_MAP: Record = { @@ -20,7 +21,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields, [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, - [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields, + [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/OktaClientSecretRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/OktaClientSecretRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..72adc863d --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/OktaClientSecretRotationSecretsMappingFields.tsx @@ -0,0 +1,58 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2"; + +import { SecretsMappingTable } from "./shared"; + +export const OktaClientSecretRotationSecretsMappingFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.OktaClientSecret; + } + >(); + + const { rotationOption } = useSecretRotationV2Option(SecretRotation.OktaClientSecret); + + const items = [ + { + name: "Client ID", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.clientId" + /> + ) + }, + { + name: "Client Secret", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.clientSecret" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 428a99161..dd0ce9cab 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecr import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields"; import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields"; import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields"; +import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields"; import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; const COMPONENT_MAP: Record = { @@ -17,7 +18,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields, [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, - [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields, + [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 77151bf1e..a6ebe2f64 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -10,6 +10,7 @@ import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotati import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; +import { OktaClientSecretRotationSchema } from "./okta-client-secret-rotation-schema"; import { OracleDBCredentialsRotationSchema } from "./oracledb-credentials-rotation-schema"; export const SecretRotationV2FormSchema = (isUpdate: boolean) => @@ -23,7 +24,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) => MySqlCredentialsRotationSchema, OracleDBCredentialsRotationSchema, LdapPasswordRotationSchema, - AwsIamUserSecretRotationSchema + AwsIamUserSecretRotationSchema, + OktaClientSecretRotationSchema ]), z.object({ id: z.string().optional() }) ) diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/okta-client-secret-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/okta-client-secret-rotation-schema.ts new file mode 100644 index 000000000..569ee2c6c --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/okta-client-secret-rotation-schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const OktaClientSecretRotationSchema = z + .object({ + type: z.literal(SecretRotation.OktaClientSecret), + parameters: z.object({ + clientId: z.string().trim().min(1, "App ID required") + }), + secretsMapping: z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client Secret required") + }) + }) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/BitbucketSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/BitbucketSyncFields.tsx new file mode 100644 index 000000000..330d75811 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/BitbucketSyncFields.tsx @@ -0,0 +1,141 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { + TBitbucketEnvironment, + TBitbucketRepo, + TBitbucketWorkspace, + useBitbucketConnectionListEnvironments, + useBitbucketConnectionListRepositories, + useBitbucketConnectionListWorkspaces +} from "@app/hooks/api/appConnections/bitbucket"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const BitbucketSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Bitbucket } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const workspace = useWatch({ name: "destinationConfig.workspaceSlug", control }); + const repository = useWatch({ name: "destinationConfig.repositorySlug", control }); + + const { data: workspaces = [], isPending: isWorkspacesLoading } = + useBitbucketConnectionListWorkspaces(connectionId, { + enabled: Boolean(connectionId) + }); + + const { data: repositories = [], isPending: isRepositoriesLoading } = + useBitbucketConnectionListRepositories(connectionId, workspace ?? "", { + enabled: Boolean(connectionId) && Boolean(workspace) + }); + + const { data: environments = [], isPending: isEnvironmentsLoading } = + useBitbucketConnectionListEnvironments(connectionId, workspace ?? "", repository ?? "", { + enabled: Boolean(connectionId) && Boolean(workspace) && Boolean(repository) + }); + + return ( + <> + { + setValue("destinationConfig.workspaceSlug", ""); + setValue("destinationConfig.repositorySlug", ""); + setValue("destinationConfig.environmentId", ""); + }} + /> + + ( + + w.slug === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.slug ?? ""); + // Clear downstream selections + setValue("destinationConfig.repositorySlug", ""); + setValue("destinationConfig.environmentId", ""); + }} + options={workspaces} + placeholder="Select workspace..." + getOptionLabel={(option) => option.slug} + getOptionValue={(option) => option.slug} + /> + + )} + /> + + ( + + r.slug === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.slug ?? ""); + // Clear downstream selections + setValue("destinationConfig.environmentId", ""); + }} + options={repositories} + placeholder="Select repository..." + getOptionLabel={(option) => option.full_name} + getOptionValue={(option) => option.slug} + /> + + )} + /> + + ( + + e.uuid === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.uuid ?? ""); + }} + options={environments} + placeholder="Select environment..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.uuid} + isClearable + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 09cf2caec..c933fa44d 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -9,6 +9,7 @@ import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields"; import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields"; import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; +import { BitbucketSyncFields } from "./BitbucketSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { ChecklySyncFields } from "./ChecklySyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; @@ -91,6 +92,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Supabase: return ; + case SecretSync.Bitbucket: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index eaf66a053..e7226eb68 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -63,6 +63,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Railway: case SecretSync.Checkly: case SecretSync.Supabase: + case SecretSync.Bitbucket: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitbucketSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitbucketSyncReviewFields.tsx new file mode 100644 index 000000000..93630f225 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitbucketSyncReviewFields.tsx @@ -0,0 +1,20 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const BitbucketSyncReviewFields = () => { + const { watch } = useFormContext(); + const repository = watch("destinationConfig.repositorySlug"); + const environment = watch("destinationConfig.environmentId"); + const workspace = watch("destinationConfig.workspaceSlug"); + + return ( + <> + {repository} + {environment} + {workspace} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index d09f58a2e..d785dc87c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -18,6 +18,7 @@ import { import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; +import { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; @@ -144,6 +145,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Supabase: DestinationFieldsComponent = ; break; + case SecretSync.Bitbucket: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/bitbucket-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/bitbucket-sync-destination-schema.ts new file mode 100644 index 000000000..a10a22d5a --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/bitbucket-sync-destination-schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const BitbucketSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Bitbucket), + destinationConfig: z.object({ + repositorySlug: z + .string() + .trim() + .min(1, "Repository slug required") + .describe("Repository slug"), + environmentId: z.string().trim().optional().describe("Deployment environment uuid"), + workspaceSlug: z.string().trim().min(1, "Workspace slug required").describe("Workspace slug") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 83c334f71..dc082e168 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -6,6 +6,7 @@ import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sy import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; +import { BitbucketSyncDestinationSchema } from "./bitbucket-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema"; import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; @@ -55,7 +56,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ SupabaseSyncDestinationSchema, ZabbixSyncDestinationSchema, RailwaySyncDestinationSchema, - ChecklySyncDestinationSchema + ChecklySyncDestinationSchema, + BitbucketSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/v2/CopyButton/CopyButton.tsx b/frontend/src/components/v2/CopyButton/CopyButton.tsx index ff1161ca9..135dec366 100644 --- a/frontend/src/components/v2/CopyButton/CopyButton.tsx +++ b/frontend/src/components/v2/CopyButton/CopyButton.tsx @@ -41,7 +41,8 @@ export const CopyButton = ({ variant={variant} className={twMerge("group relative", color)} size={size} - onClick={() => { + onClick={(e) => { + e.stopPropagation(); handleCopyText(); }} > diff --git a/frontend/src/components/v2/DatePicker/DatePicker.tsx b/frontend/src/components/v2/DatePicker/DatePicker.tsx index 028d6623a..115b0ad3e 100644 --- a/frontend/src/components/v2/DatePicker/DatePicker.tsx +++ b/frontend/src/components/v2/DatePicker/DatePicker.tsx @@ -1,11 +1,13 @@ import { ChangeEventHandler, useState } from "react"; -import { DayPicker, DayPickerProps, getDefaultClassNames, UI } from "react-day-picker"; +import { DayPicker, DayPickerProps, getDefaultClassNames, TZDate, UI } from "react-day-picker"; import { faCalendar } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover"; import { format, setHours, setMinutes } from "date-fns"; import { twMerge } from "tailwind-merge"; +import { formatDateTime, Timezone } from "@app/helpers/datetime"; + import { Button } from "../Button"; import { Input } from "../Input"; import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2"; @@ -19,6 +21,32 @@ export type DatePickerProps = Omit & { popUpContentProps: PopoverContentProps; dateFormat?: "PPP" | "PP" | "P"; // extend as needed hideTime?: boolean; + buttonClassName?: string; + timezone?: Timezone; +}; + +const localTimeToUTC = (timeString: string) => { + const today = new Date(); + const [hours, minutes] = timeString.split(":").map(Number); + + today.setHours(hours, minutes, 0, 0); + + const utcHours = today.getUTCHours().toString().padStart(2, "0"); + const utcMinutes = today.getUTCMinutes().toString().padStart(2, "0"); + + return `${utcHours}:${utcMinutes}`; +}; + +const utcTimeToLocal = (utcTimeString: string) => { + const today = new Date(); + const [hours, minutes] = utcTimeString.split(":").map(Number); + + today.setUTCHours(hours, minutes, 0, 0); + + const localHours = today.getHours().toString().padStart(2, "0"); + const localMinutes = today.getMinutes().toString().padStart(2, "0"); + + return `${localHours}:${localMinutes}`; }; // Doc: https://react-day-picker.js.org/ @@ -29,12 +57,16 @@ export const DatePicker = ({ popUpContentProps, dateFormat = "PPP", hideTime = false, + buttonClassName, + timezone, ...props }: DatePickerProps) => { const [timeValue, setTimeValue] = useState(value ? format(value, "HH:mm") : "00:00"); + const displayUtc = timezone === Timezone.UTC; const handleTimeChange: ChangeEventHandler = (e) => { - const time = e.target.value; + const time = displayUtc ? utcTimeToLocal(e.target.value) : e.target.value; + if (time) { setTimeValue(time); if (value) { @@ -59,8 +91,14 @@ export const DatePicker = ({ return ( - handleDaySelect(date ? new TZDate(date, undefined) : undefined)} className="font-inter text-mineshaft-200" + timeZone={displayUtc ? "UTC" : undefined} classNames={{ today: "text-primary border-primary", selected: " text-mineshaft-100 bg-mineshaft-500", @@ -91,7 +130,7 @@ export const DatePicker = ({
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 91c376662..f114c0f02 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -19,6 +19,7 @@ type Props = { icon?: IconProp; isMulti?: boolean; iconClassName?: string; + dropdownContainerStyle?: React.CSSProperties; }; export type SelectProps = Omit & Props; @@ -35,6 +36,7 @@ export const Select = forwardRef( position, containerClassName, iconClassName, + dropdownContainerStyle, ...props }, ref @@ -82,7 +84,7 @@ export const Select = forwardRef( dropdownContainerClassName )} position={position} - style={{ width: "var(--radix-select-trigger-width)" }} + style={dropdownContainerStyle ?? { width: "var(--radix-select-trigger-width)" }} >
diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index a90699087..47c7c3d24 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -70,276 +70,276 @@ export const ROUTE_PATHS = Object.freeze({ }, SecretManager: { ApprovalPage: setRoute( - "/projects/$projectId/secret-manager/approval", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/approval" + "/projects/secret-management/$projectId/approval", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval" ), SecretDashboardPage: setRoute( - "/projects/$projectId/secret-manager/secrets/$envSlug", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/secrets/$envSlug" + "/projects/secret-management/$projectId/secrets/$envSlug", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug" ), RollbackPreviewPage: setRoute( - "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId/restore", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore" + "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore" ), CommitDetailsPage: setRoute( - "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId" + "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId" ), CommitsPage: setRoute( - "/projects/$projectId/secret-manager/commits/$environment/$folderId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId" + "/projects/secret-management/$projectId/commits/$environment/$folderId", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId" ), OverviewPage: setRoute( - "/projects/$projectId/secret-manager/overview", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview" + "/projects/secret-management/$projectId/overview", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview" ), IntegrationsListPage: setRoute( - "/projects/$projectId/secret-manager/integrations", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/" + "/projects/secret-management/$projectId/integrations", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/" ), IntegrationDetailsByIDPage: setRoute( - "/projects/$projectId/secret-manager/integrations/$integrationId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/$integrationId" + "/projects/secret-management/$projectId/integrations/$integrationId", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId" ), SecretSyncDetailsByIDPage: setRoute( - "/projects/$projectId/secret-manager/integrations/secret-syncs/$destination/$syncId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId" + "/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId" ), Integratons: { SelectIntegrationAuth: setRoute( - "/projects/$projectId/secret-manager/integrations/select-integration-auth", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/select-integration-auth" + "/projects/secret-management/$projectId/integrations/select-integration-auth", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth" ), HerokuOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/heroku/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/oauth2/callback" + "/projects/secret-management/$projectId/integrations/heroku/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback" ), HerokuConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/heroku/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/create" + "/projects/secret-management/$projectId/integrations/heroku/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/create" ), AwsParameterStoreConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/aws-parameter-store/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-parameter-store/create" + "/projects/secret-management/$projectId/integrations/aws-parameter-store/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create" ), AwsSecretManagerConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/aws-secret-manager/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-secret-manager/create" + "/projects/secret-management/$projectId/integrations/aws-secret-manager/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create" ), AzureAppConfigurationsOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/azure-app-configuration/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback" + "/projects/secret-management/$projectId/integrations/azure-app-configuration/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback" ), AzureAppConfigurationsConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/azure-app-configuration/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/create" + "/projects/secret-management/$projectId/integrations/azure-app-configuration/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create" ), AzureDevopsConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/azure-devops/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-devops/create" + "/projects/secret-management/$projectId/integrations/azure-devops/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create" ), AzureKeyVaultAuthorizePage: setRoute( - "/projects/$projectId/secret-manager/integrations/azure-key-vault/authorize", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/authorize" + "/projects/secret-management/$projectId/integrations/azure-key-vault/authorize", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize" ), AzureKeyVaultOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/azure-key-vault/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback" + "/projects/secret-management/$projectId/integrations/azure-key-vault/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback" ), AzureKeyVaultConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/azure-key-vault/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/create" + "/projects/secret-management/$projectId/integrations/azure-key-vault/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create" ), BitbucketOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/bitbucket/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/oauth2/callback" + "/projects/secret-management/$projectId/integrations/bitbucket/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback" ), BitbucketConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/bitbucket/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/create" + "/projects/secret-management/$projectId/integrations/bitbucket/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create" ), ChecklyConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/checkly/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/checkly/create" + "/projects/secret-management/$projectId/integrations/checkly/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create" ), CircleConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/circleci/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/circleci/create" + "/projects/secret-management/$projectId/integrations/circleci/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create" ), CloudflarePagesConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/cloudflare-pages/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-pages/create" + "/projects/secret-management/$projectId/integrations/cloudflare-pages/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create" ), DigitalOceanAppPlatformConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/digital-ocean-app-platform/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/digital-ocean-app-platform/create" + "/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create" ), CloudflareWorkersConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/cloudflare-workers/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-workers/create" + "/projects/secret-management/$projectId/integrations/cloudflare-workers/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create" ), CodefreshConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/codefresh/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/codefresh/create" + "/projects/secret-management/$projectId/integrations/codefresh/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/codefresh/create" ), GcpSecretManagerConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/gcp-secret-manager/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/create" + "/projects/secret-management/$projectId/integrations/gcp-secret-manager/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create" ), GcpSecretManagerOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/gcp-secret-manager/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback" + "/projects/secret-management/$projectId/integrations/gcp-secret-manager/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback" ), GithubConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/github/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/create" + "/projects/secret-management/$projectId/integrations/github/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/create" ), GithubOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/github/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/oauth2/callback" + "/projects/secret-management/$projectId/integrations/github/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/oauth2/callback" ), GitlabConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/gitlab/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/create" + "/projects/secret-management/$projectId/integrations/gitlab/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/create" ), GitlabOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/gitlab/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/oauth2/callback" + "/projects/secret-management/$projectId/integrations/gitlab/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback" ), VercelOauthCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/vercel/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/oauth2/callback" + "/projects/secret-management/$projectId/integrations/vercel/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback" ), VercelConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/vercel/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/create" + "/projects/secret-management/$projectId/integrations/vercel/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/create" ), FlyioConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/flyio/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/flyio/create" + "/projects/secret-management/$projectId/integrations/flyio/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/flyio/create" ), HashicorpVaultConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/hashicorp-vault/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hashicorp-vault/create" + "/projects/secret-management/$projectId/integrations/hashicorp-vault/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create" ), HasuraCloudConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/hasura-cloud/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hasura-cloud/create" + "/projects/secret-management/$projectId/integrations/hasura-cloud/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hasura-cloud/create" ), LaravelForgeConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/laravel-forge/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/laravel-forge/create" + "/projects/secret-management/$projectId/integrations/laravel-forge/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/laravel-forge/create" ), NorthflankConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/northflank/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/northflank/create" + "/projects/secret-management/$projectId/integrations/northflank/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/northflank/create" ), RailwayConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/railway/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/railway/create" + "/projects/secret-management/$projectId/integrations/railway/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/railway/create" ), RenderConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/render/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/render/create" + "/projects/secret-management/$projectId/integrations/render/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/render/create" ), RundeckConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/rundeck/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/rundeck/create" + "/projects/secret-management/$projectId/integrations/rundeck/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/rundeck/create" ), WindmillConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/windmill/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/windmill/create" + "/projects/secret-management/$projectId/integrations/windmill/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/windmill/create" ), TravisCIConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/travisci/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/travisci/create" + "/projects/secret-management/$projectId/integrations/travisci/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/travisci/create" ), TerraformCloudConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/terraform-cloud/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/terraform-cloud/create" + "/projects/secret-management/$projectId/integrations/terraform-cloud/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/terraform-cloud/create" ), TeamcityConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/teamcity/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/teamcity/create" + "/projects/secret-management/$projectId/integrations/teamcity/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/teamcity/create" ), SupabaseConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/supabase/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/supabase/create" + "/projects/secret-management/$projectId/integrations/supabase/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/supabase/create" ), OctopusDeployCloudConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/octopus-deploy/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/octopus-deploy/create" + "/projects/secret-management/$projectId/integrations/octopus-deploy/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/octopus-deploy/create" ), DatabricksConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/databricks/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/databricks/create" + "/projects/secret-management/$projectId/integrations/databricks/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/databricks/create" ), QoveryConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/qovery/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/qovery/create" + "/projects/secret-management/$projectId/integrations/qovery/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/qovery/create" ), Cloud66ConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/cloud-66/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloud-66/create" + "/projects/secret-management/$projectId/integrations/cloud-66/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create" ), NetlifyConfigurePage: setRoute( - "/projects/$projectId/secret-manager/integrations/netlify/create", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/create" + "/projects/secret-management/$projectId/integrations/netlify/create", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/create" ), NetlifyOuathCallbackPage: setRoute( - "/projects/$projectId/secret-manager/integrations/netlify/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/oauth2/callback" + "/projects/secret-management/$projectId/integrations/netlify/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback" ) } }, CertManager: { CertAuthDetailsByIDPage: setRoute( - "/projects/$projectId/cert-manager/ca/$caName", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName" + "/projects/cert-management/$projectId/ca/$caName", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName" ), SubscribersPage: setRoute( - "/projects/$projectId/cert-manager/subscribers", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers" + "/projects/cert-management/$projectId/subscribers", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers" ), CertificatesPage: setRoute( - "/projects/$projectId/cert-manager/certificates", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates" + "/projects/cert-management/$projectId/certificates", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates" ), CertificateAuthoritiesPage: setRoute( - "/projects/$projectId/cert-manager/certificate-authorities", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities" + "/projects/cert-management/$projectId/certificate-authorities", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities" ), AlertingPage: setRoute( - "/projects/$projectId/cert-manager/alerting", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting" + "/projects/cert-management/$projectId/alerting", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting" ), PkiCollectionDetailsByIDPage: setRoute( - "/projects/$projectId/cert-manager/pki-collections/$collectionId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId" + "/projects/cert-management/$projectId/pki-collections/$collectionId", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId" ), PkiSubscriberDetailsByIDPage: setRoute( - "/projects/$projectId/cert-manager/subscribers/$subscriberName", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName" + "/projects/cert-management/$projectId/subscribers/$subscriberName", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName" ) }, Ssh: { SshCaByIDPage: setRoute( - "/projects/$projectId/ssh/ca/$caId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ca/$caId" + "/projects/ssh/$projectId/ca/$caId", + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ca/$caId" ), SshHostGroupDetailsByIDPage: setRoute( - "/projects/$projectId/ssh/ssh-host-groups/$sshHostGroupId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ssh-host-groups/$sshHostGroupId" + "/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId", + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId" ) }, SecretScanning: { DataSourceByIdPage: setRoute( - "/projects/$projectId/secret-scanning/data-sources/$type/$dataSourceId", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/$type/$dataSourceId" + "/projects/secret-scanning/$projectId/data-sources/$type/$dataSourceId", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId" ), FindingsPage: setRoute( - "/projects/$projectId/secret-scanning/findings", - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/findings" + "/projects/secret-scanning/$projectId/findings", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings" ) }, Public: { diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 996483904..807569c6e 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -17,7 +17,9 @@ import { AzureClientSecretsConnectionMethod, AzureDevOpsConnectionMethod, AzureKeyVaultConnectionMethod, + BitbucketConnectionMethod, CamundaConnectionMethod, + ChecklyConnectionMethod, CloudflareConnectionMethod, DatabricksConnectionMethod, FlyioConnectionMethod, @@ -26,13 +28,19 @@ import { GitHubRadarConnectionMethod, GitLabConnectionMethod, HCVaultConnectionMethod, + HerokuConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, + OCIConnectionMethod, + OktaConnectionMethod, OnePassConnectionMethod, OracleDBConnectionMethod, PostgresConnectionMethod, + RailwayConnectionMethod, + RenderConnectionMethod, + SupabaseConnectionMethod, TAppConnection, TeamCityConnectionMethod, TerraformCloudConnectionMethod, @@ -40,13 +48,6 @@ import { WindmillConnectionMethod, ZabbixConnectionMethod } from "@app/hooks/api/appConnections/types"; -import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; -import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; -import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; -import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; -import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection"; -import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; -import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -98,7 +99,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" }, - [AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" } + [AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }, + [AppConnection.Okta]: { name: "Okta", image: "Okta.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -132,6 +134,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case CloudflareConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case OktaConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/helpers/datetime.ts b/frontend/src/helpers/datetime.ts new file mode 100644 index 000000000..6e0fb8e48 --- /dev/null +++ b/frontend/src/helpers/datetime.ts @@ -0,0 +1,24 @@ +import { format } from "date-fns"; + +export enum Timezone { + Local = "local", + UTC = "UTC" +} + +export const formatDateTime = ({ + timezone, + timestamp, + dateFormat = "MMM do yyyy, hh:mm a" +}: { + timestamp: string | Date; + timezone?: Timezone; + dateFormat?: string; +}) => { + const date = new Date(timestamp); + + if (timezone === Timezone.UTC) { + const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000); + return `${format(utcDate, dateFormat)}`; + } + return format(date, dateFormat); +}; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index e75bb63de..3b04b263d 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -42,7 +42,8 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } const { data: { project } } = await createWorkspace({ - projectName + projectName, + type: ProjectType.SecretManager }); try { @@ -59,20 +60,34 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } return project; }; + +export const getProjectBaseURL = (type: ProjectType) => { + switch (type) { + case ProjectType.SecretManager: + return "/projects/secret-management/$projectId"; + case ProjectType.CertificateManager: + return "/projects/cert-management/$projectId"; + default: + return `/projects/${type}/$projectId` as const; + } +}; + export const getProjectHomePage = (type: ProjectType) => { switch (type) { + case ProjectType.SecretManager: + return "/projects/secret-management/$projectId/overview"; case ProjectType.CertificateManager: - return `/projects/$projectId/${type}/subscribers` as const; + return "/projects/cert-management/$projectId/subscribers"; case ProjectType.SecretScanning: - return `/projects/$projectId/${type}/data-sources` as const; + return `/projects/${type}/$projectId/data-sources` as const; default: - return `/projects/$projectId/${type}/overview` as const; + return `/projects/${type}/$projectId/overview` as const; } }; export const getProjectTitle = (type: ProjectType) => { const titleConvert = { - [ProjectType.SecretManager]: "Secret Management", + [ProjectType.SecretManager]: "Secrets Management", [ProjectType.KMS]: "Key Management", [ProjectType.CertificateManager]: "Cert Management", [ProjectType.SSH]: "SSH", @@ -81,7 +96,13 @@ export const getProjectTitle = (type: ProjectType) => { return titleConvert[type]; }; -export const getCurrentProductFromUrl = (location: string) => { - const type = Object.values(ProjectType).find((el) => location.includes(`/${el}`)); - return type; +export const getProjectLottieIcon = (type: ProjectType) => { + const titleConvert = { + [ProjectType.SecretManager]: "vault", + [ProjectType.KMS]: "unlock", + [ProjectType.CertificateManager]: "note", + [ProjectType.SSH]: "terminal", + [ProjectType.SecretScanning]: "secret-scan" + }; + return titleConvert[type]; }; diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index 6e484881d..2979a7623 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -44,6 +44,11 @@ export const SECRET_ROTATION_MAP: Record< name: "AWS IAM User Secret", image: "Amazon Web Services.png", size: 50 + }, + [SecretRotation.OktaClientSecret]: { + name: "Okta Client Secret", + image: "Okta.png", + size: 50 } }; @@ -55,7 +60,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.Auth0ClientSecret]: false, [SecretRotation.AzureClientSecret]: true, [SecretRotation.LdapPassword]: false, - [SecretRotation.AwsIamUserSecret]: true + [SecretRotation.AwsIamUserSecret]: true, + [SecretRotation.OktaClientSecret]: true }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 29b4f2f73..cabd7fa6e 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -101,6 +101,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Supabase]: AppConnection.Supabase, [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway, - [SecretSync.Checkly]: AppConnection.Checkly + [SecretSync.Checkly]: AppConnection.Checkly, + [SecretSync.Bitbucket]: AppConnection.Bitbucket }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx index bfae0534f..d4f85bcf7 100644 --- a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx +++ b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx @@ -4,8 +4,10 @@ import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "../queries"; import { + TBitbucketConnectionListEnvironmentsResponse, TBitbucketConnectionListRepositoriesResponse, TBitbucketConnectionListWorkspacesResponse, + TBitbucketEnvironment, TBitbucketRepo, TBitbucketWorkspace } from "./types"; @@ -15,7 +17,9 @@ const bitbucketConnectionKeys = { listRepos: (connectionId: string, workspaceSlug: string) => [...bitbucketConnectionKeys.all, "repos", connectionId, workspaceSlug] as const, listWorkspaces: (connectionId: string) => - [...bitbucketConnectionKeys.all, "workspaces", connectionId] as const + [...bitbucketConnectionKeys.all, "workspaces", connectionId] as const, + listEnvironments: (connectionId: string, workspaceSlug: string, repoSlug: string) => + [...bitbucketConnectionKeys.all, "environments", connectionId, workspaceSlug, repoSlug] as const }; export const useBitbucketConnectionListWorkspaces = ( @@ -68,3 +72,30 @@ export const useBitbucketConnectionListRepositories = ( ...options }); }; + +export const useBitbucketConnectionListEnvironments = ( + connectionId: string, + workspaceSlug: string, + repoSlug: string, + options?: Omit< + UseQueryOptions< + TBitbucketEnvironment[], + unknown, + TBitbucketEnvironment[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: bitbucketConnectionKeys.listEnvironments(connectionId, workspaceSlug, repoSlug), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/bitbucket/${connectionId}/environments?workspaceSlug=${encodeURIComponent(workspaceSlug)}&repositorySlug=${encodeURIComponent(repoSlug)}` + ); + + return data.environments; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/bitbucket/types.ts b/frontend/src/hooks/api/appConnections/bitbucket/types.ts index e7e653a93..79ebbc7cc 100644 --- a/frontend/src/hooks/api/appConnections/bitbucket/types.ts +++ b/frontend/src/hooks/api/appConnections/bitbucket/types.ts @@ -15,3 +15,13 @@ export type TBitbucketConnectionListWorkspacesResponse = { export type TBitbucketConnectionListRepositoriesResponse = { repositories: TBitbucketRepo[]; }; + +export type TBitbucketEnvironment = { + uuid: string; + name: string; + slug: string; +}; + +export type TBitbucketConnectionListEnvironmentsResponse = { + environments: TBitbucketEnvironment[]; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 965675bc7..2e1e59655 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -32,5 +32,6 @@ export enum AppConnection { Zabbix = "zabbix", Railway = "railway", Checkly = "checkly", - Supabase = "supabase" + Supabase = "supabase", + Okta = "okta" } diff --git a/frontend/src/hooks/api/appConnections/okta/index.ts b/frontend/src/hooks/api/appConnections/okta/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/okta/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/okta/queries.tsx b/frontend/src/hooks/api/appConnections/okta/queries.tsx new file mode 100644 index 000000000..9823a296a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/okta/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TOktaApp } from "./types"; + +const oktaConnectionKeys = { + all: [...appConnectionKeys.all, "okta"] as const, + listApps: (connectionId: string) => [...oktaConnectionKeys.all, "apps", connectionId] as const +}; + +export const useOktaConnectionListApps = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TOktaApp[], + unknown, + TOktaApp[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: oktaConnectionKeys.listApps(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ apps: TOktaApp[] }>( + `/api/v1/app-connections/okta/${connectionId}/apps` + ); + + return data.apps; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/okta/types.ts b/frontend/src/hooks/api/appConnections/okta/types.ts new file mode 100644 index 000000000..8acc6c04d --- /dev/null +++ b/frontend/src/hooks/api/appConnections/okta/types.ts @@ -0,0 +1,4 @@ +export type TOktaApp = { + id: string; + label: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index b6b00a615..e9bc5b243 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -152,6 +152,10 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Supabase; }; +export type TOktaConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Okta; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -183,7 +187,8 @@ export type TAppConnectionOption = | TBitbucketConnectionOption | TZabbixConnectionOption | TRailwayConnectionOption - | TChecklyConnectionOption; + | TChecklyConnectionOption + | TOktaConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -220,4 +225,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Checkly]: TChecklyConnectionOption; [AppConnection.Supabase]: TSupabaseConnectionOption; + [AppConnection.Okta]: TOktaConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index e177fa2ab..5085feab3 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -24,6 +24,7 @@ import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TMySqlConnection } from "./mysql-connection"; import { TOCIConnection } from "./oci-connection"; +import { TOktaConnection } from "./okta-connection"; import { TOracleDBConnection } from "./oracledb-connection"; import { TPostgresConnection } from "./postgres-connection"; import { TRailwayConnection } from "./railway-connection"; @@ -44,6 +45,7 @@ export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; export * from "./bitbucket-connection"; export * from "./camunda-connection"; +export * from "./checkly-connection"; export * from "./cloudflare-connection"; export * from "./databricks-connection"; export * from "./flyio-connection"; @@ -58,9 +60,12 @@ export * from "./ldap-connection"; export * from "./mssql-connection"; export * from "./mysql-connection"; export * from "./oci-connection"; +export * from "./okta-connection"; export * from "./oracledb-connection"; export * from "./postgres-connection"; +export * from "./railway-connection"; export * from "./render-connection"; +export * from "./supabase-connection"; export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; @@ -101,7 +106,8 @@ export type TAppConnection = | TZabbixConnection | TRailwayConnection | TChecklyConnection - | TSupabaseConnection; + | TSupabaseConnection + | TOktaConnection; export type TAvailableAppConnection = Pick; @@ -113,11 +119,20 @@ export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppCo export type TCreateAppConnectionDTO = Pick< TAppConnection, - "name" | "credentials" | "method" | "app" | "description" | "isPlatformManagedCredentials" + | "name" + | "credentials" + | "method" + | "app" + | "description" + | "isPlatformManagedCredentials" + | "gatewayId" >; export type TUpdateAppConnectionDTO = Partial< - Pick + Pick< + TAppConnection, + "name" | "credentials" | "description" | "isPlatformManagedCredentials" | "gatewayId" + > > & { connectionId: string; app: AppConnection; @@ -163,4 +178,5 @@ export type TAppConnectionMap = { [AppConnection.Railway]: TRailwayConnection; [AppConnection.Checkly]: TChecklyConnection; [AppConnection.Supabase]: TSupabaseConnection; + [AppConnection.Okta]: TOktaConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/okta-connection.ts b/frontend/src/hooks/api/appConnections/types/okta-connection.ts new file mode 100644 index 000000000..a622388b4 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/okta-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum OktaConnectionMethod { + ApiToken = "api-token" +} + +export type TOktaConnection = TRootAppConnection & { app: AppConnection.Okta } & { + method: OktaConnectionMethod.ApiToken; + credentials: { + instanceUrl: string; + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/root-connection.ts b/frontend/src/hooks/api/appConnections/types/root-connection.ts index 52571d067..5b8f5cd02 100644 --- a/frontend/src/hooks/api/appConnections/types/root-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/root-connection.ts @@ -7,4 +7,5 @@ export type TRootAppConnection = { createdAt: string; updatedAt: string; isPlatformManagedCredentials?: boolean; + gatewayId?: string | null; }; diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 52a35cb85..b254b46d3 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -57,6 +57,7 @@ export const useGetAuditLogs = ( }, getNextPageParam: (lastPage, pages) => lastPage.length !== 0 ? pages.length * filters.limit : undefined, + placeholderData: (prev) => prev, ...options }); }; diff --git a/frontend/src/hooks/api/folderCommits/queries.tsx b/frontend/src/hooks/api/folderCommits/queries.tsx index 8d0883c5b..1bca7ce9a 100644 --- a/frontend/src/hooks/api/folderCommits/queries.tsx +++ b/frontend/src/hooks/api/folderCommits/queries.tsx @@ -1,8 +1,9 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { format } from "date-fns"; import { apiRequest } from "@app/config/request"; -import { CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types"; +import { Commit, CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types"; export const commitKeys = { count: ({ @@ -242,7 +243,6 @@ export const useGetFolderCommitHistory = ({ workspaceId, environment, directory, - offset = 0, limit = 20, search, sort = "desc" @@ -250,22 +250,33 @@ export const useGetFolderCommitHistory = ({ workspaceId: string; environment: string; directory: string; - offset?: number; limit?: number; search?: string; sort?: "asc" | "desc"; }) => { - return useQuery({ - queryKey: [ - commitKeys.history({ workspaceId, environment, directory }), - offset, - limit, - search, - sort - ], - queryFn: () => - fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort), - enabled: Boolean(workspaceId && environment) + return useInfiniteQuery({ + initialPageParam: 0, + queryKey: [commitKeys.history({ workspaceId, environment, directory }), limit, search, sort], + queryFn: ({ pageParam }) => + fetchFolderCommitHistory(workspaceId, environment, directory, pageParam, limit, search, sort), + enabled: Boolean(workspaceId && environment), + select: (data) => { + return (data?.pages ?? []) + ?.map((page) => page.commits) + .flat() + .reduce( + (acc, commit) => { + const date = format(new Date(commit.createdAt), "MMM d, yyyy"); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(commit); + return acc; + }, + {} as Record + ); + }, + getNextPageParam: (lastPage, pages) => (lastPage.hasMore ? pages.length * limit : undefined) }); }; diff --git a/frontend/src/hooks/api/folderCommits/types.ts b/frontend/src/hooks/api/folderCommits/types.ts index 878e3224d..a5f4e6df6 100644 --- a/frontend/src/hooks/api/folderCommits/types.ts +++ b/frontend/src/hooks/api/folderCommits/types.ts @@ -62,3 +62,16 @@ export type RollbackPreview = { folderPath: string; changes: RollbackChange[]; }; + +interface CommitActorMetadata { + email?: string; + name?: string; +} + +export interface Commit { + id: string; + message: string; + createdAt: string; + actorType: string; + actorMetadata?: CommitActorMetadata; +} diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 69cb26e6c..1af1cc24c 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -51,7 +51,7 @@ export type IdentityMembershipOrg = { export type IdentityMembership = { id: string; identity: Identity; - project: Pick; + project: Pick; roles: Array< { id: string; diff --git a/frontend/src/hooks/api/projectTemplates/types.ts b/frontend/src/hooks/api/projectTemplates/types.ts index e9f76f149..f5ff22dff 100644 --- a/frontend/src/hooks/api/projectTemplates/types.ts +++ b/frontend/src/hooks/api/projectTemplates/types.ts @@ -1,9 +1,12 @@ import { TProjectRole } from "@app/hooks/api/roles/types"; +import { ProjectType } from "../workspace/types"; + export type TProjectTemplate = { id: string; name: string; description?: string; + type: ProjectType; roles: Pick[]; environments?: { name: string; slug: string; position: number }[] | null; createdAt: string; @@ -16,6 +19,7 @@ export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate }; export type TCreateProjectTemplateDTO = { name: string; description?: string; + type?: ProjectType; }; export type TUpdateProjectTemplateDTO = Partial< diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index 33653ff72..dc9bce70f 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -4,11 +4,19 @@ export enum ReservedFolders { SecretReplication = "__reserve_replication_" } +export enum PendingAction { + Create = "create", + Update = "update", + Delete = "delete" +} + export type TSecretFolder = { id: string; name: string; description?: string; parentId?: string | null; + isPending?: boolean; + pendingAction?: PendingAction; }; export type TSecretFolderWithPath = TSecretFolder & { path: string }; diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index bb2765ffd..be692cee3 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -6,7 +6,8 @@ export enum SecretRotation { Auth0ClientSecret = "auth0-client-secret", AzureClientSecret = "azure-client-secret", LdapPassword = "ldap-password", - AwsIamUserSecret = "aws-iam-user-secret" + AwsIamUserSecret = "aws-iam-user-secret", + OktaClientSecret = "okta-client-secret" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index e4b3b6ee1..06783944b 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -35,6 +35,11 @@ import { TMySqlCredentialsRotation, TMySqlCredentialsRotationGeneratedCredentialsResponse } from "./mysql-credentials-rotation"; +import { + TOktaClientSecretRotation, + TOktaClientSecretRotationGeneratedCredentialsResponse, + TOktaClientSecretRotationOption +} from "./okta-client-secret-rotation"; import { TOracleDBCredentialsRotation, TOracleDBCredentialsRotationGeneratedCredentialsResponse @@ -49,6 +54,7 @@ export type TSecretRotationV2 = ( | TAzureClientSecretRotation | TLdapPasswordRotation | TAwsIamUserSecretRotation + | TOktaClientSecretRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; @@ -58,7 +64,8 @@ export type TSecretRotationV2Option = | TAuth0ClientSecretRotationOption | TAzureClientSecretRotationOption | TLdapPasswordRotationOption - | TAwsIamUserSecretRotationOption; + | TAwsIamUserSecretRotationOption + | TOktaClientSecretRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -72,7 +79,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse = | TAuth0ClientSecretRotationGeneratedCredentialsResponse | TAzureClientSecretRotationGeneratedCredentialsResponse | TLdapPasswordRotationGeneratedCredentialsResponse - | TAwsIamUserSecretRotationGeneratedCredentialsResponse; + | TAwsIamUserSecretRotationGeneratedCredentialsResponse + | TOktaClientSecretRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -124,6 +132,7 @@ export type TSecretRotationOptionMap = { [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption; [SecretRotation.LdapPassword]: TLdapPasswordRotationOption; [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption; + [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { @@ -135,4 +144,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse; [SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse; [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse; + [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/okta-client-secret-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/okta-client-secret-rotation.ts new file mode 100644 index 000000000..2884f9b29 --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/okta-client-secret-rotation.ts @@ -0,0 +1,37 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TOktaClientSecretRotation = TSecretRotationV2Base & { + type: SecretRotation.OktaClientSecret; + parameters: { + clientId: string; + }; + secretsMapping: { + clientId: string; + clientSecret: string; + }; +}; + +export type TOktaClientSecretRotationGeneratedCredentials = { + clientId: string; + clientSecret: string; +}; + +export type TOktaClientSecretRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.OktaClientSecret, + TOktaClientSecretRotationGeneratedCredentials + >; + +export type TOktaClientSecretRotationOption = { + name: string; + type: SecretRotation.OktaClientSecret; + connection: AppConnection.Okta; + template: { + secretsMapping: TOktaClientSecretRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index dfba4bf4b..f29599a08 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -25,7 +25,8 @@ export enum SecretSync { Supabase = "supabase", Zabbix = "zabbix", Railway = "railway", - Checkly = "checkly" + Checkly = "checkly", + Bitbucket = "bitbucket" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/bitbucket-sync.ts b/frontend/src/hooks/api/secretSyncs/types/bitbucket-sync.ts new file mode 100644 index 000000000..829b96da3 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/bitbucket-sync.ts @@ -0,0 +1,17 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TBitbucketSync = TRootSecretSync & { + destination: SecretSync.Bitbucket; + destinationConfig: { + workspaceSlug: string; + repositorySlug: string; + environmentId?: string; + }; + connection: { + app: AppConnection.Bitbucket; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index df02872ad..daffac56a 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -8,6 +8,7 @@ import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; import { TAzureDevOpsSync } from "./azure-devops-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; +import { TBitbucketSync } from "./bitbucket-sync"; import { TCamundaSync } from "./camunda-sync"; import { TChecklySync } from "./checkly-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; @@ -63,7 +64,8 @@ export type TSecretSync = | TZabbixSync | TRailwaySync | TChecklySync - | TSupabaseSync; + | TSupabaseSync + | TBitbucketSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 82bf623e6..11ebad4b1 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -2,9 +2,14 @@ import { MutationOptions, useMutation, useQueryClient } from "@tanstack/react-qu import { apiRequest } from "@app/config/request"; import { dashboardKeys } from "@app/hooks/api/dashboard/queries"; +import { + PendingChanges, + PendingSecretUpdate +} from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.store"; import { commitKeys } from "../folderCommits/queries"; import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries"; +import { PendingAction } from "../secretFolders/types"; import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { secretKeys } from "./queries"; import { @@ -420,3 +425,102 @@ export const useBackfillSecretReference = () => return data.message; } }); + +export const useCreateCommit = () => { + const queryClient = useQueryClient(); + return useMutation< + object, + object, + { + workspaceId: string; + environment: string; + secretPath: string; + pendingChanges: PendingChanges; + message: string; + } + >({ + mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => { + const { data } = await apiRequest.post("/api/v1/pit/batch/commit", { + projectId: workspaceId, + environment, + secretPath, + changes: { + secrets: { + create: + pendingChanges.secrets + .filter((change) => change.type === PendingAction.Create) + .map((change) => ({ + secretKey: change.secretKey, + secretValue: change.secretValue, + secretComment: change.secretComment, + skipMultilineEncoding: change.skipMultilineEncoding, + tagIds: change.tags?.map((tag) => tag.id), + secretMetadata: change.secretMetadata + })) || [], + update: + pendingChanges.secrets + .filter((change) => change.type === PendingAction.Update) + .map((change: PendingSecretUpdate) => ({ + secretKey: change.secretKey, + newSecretName: change.newSecretName, + secretValue: + change.secretValue === "" + ? "" + : change.secretValue || change.existingSecret.value, + secretComment: + change.secretComment === "" + ? "" + : change.secretComment || change.existingSecret.comment, + skipMultilineEncoding: + change.skipMultilineEncoding !== undefined + ? change.skipMultilineEncoding + : change.existingSecret.skipMultilineEncoding, + tagIds: + change.tags?.map((tag) => tag.id) || + change.existingSecret.tags?.map((tag) => tag.id), + secretMetadata: change.secretMetadata || change.existingSecret.secretMetadata + })) || [], + delete: + pendingChanges.secrets.filter((change) => change.type === PendingAction.Delete) || [] + }, + folders: { + create: + pendingChanges.folders.filter((change) => change.type === PendingAction.Create) || [], + update: + pendingChanges.folders + .filter((change) => change.type === PendingAction.Update) + .map((change) => ({ + ...change, + description: change.description || null + })) || [], + delete: + pendingChanges.folders.filter((change) => change.type === PendingAction.Delete) || [] + } + }, + message + }); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries({ + queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + } + }); +}; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index fa597169f..29da8e578 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -1,5 +1,6 @@ import { ProjectPermissionActions } from "@app/context"; +import { PendingAction } from "../secretFolders/types"; import type { WsTag } from "../tags/types"; export enum SecretType { @@ -66,6 +67,8 @@ export type SecretV3RawSanitized = { isRotatedSecret?: boolean; secretReminderRecipients?: SecretReminderRecipient[]; rotationId?: string; + isPending?: boolean; + pendingAction?: PendingAction; }; export type SecretV3Raw = { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 260b02145..6408d0e22 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -255,12 +255,13 @@ export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({ - mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) => + mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) => createWorkspace({ projectName, projectDescription, kmsKeyId, - template + template, + type }), onSuccess: () => { queryClient.invalidateQueries({ @@ -280,8 +281,7 @@ export const useUpdateProject = () => { newProjectDescription, newSlug, secretSharing, - showSnapshotsLegacy, - defaultProduct + showSnapshotsLegacy }) => { const { data } = await apiRequest.patch<{ workspace: Workspace }>( `/api/v1/workspace/${projectID}`, @@ -289,7 +289,6 @@ export const useUpdateProject = () => { name: newProjectName, description: newProjectDescription, slug: newSlug, - defaultProduct, secretSharing, showSnapshotsLegacy } diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 5249f39ec..7d6f68821 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -24,7 +24,7 @@ export type Workspace = { __v: number; id: string; name: string; - defaultProduct: ProjectType; + type: ProjectType; description?: string; orgId: string; version: ProjectVersion; @@ -68,6 +68,7 @@ export type TGetUpgradeProjectStatusDTO = { // mutation dto export type CreateWorkspaceDTO = { projectName: string; + type: ProjectType; projectDescription?: string; kmsKeyId?: string; template?: string; @@ -80,7 +81,6 @@ export type UpdateProjectDTO = { newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; - defaultProduct?: ProjectType; }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; @@ -184,6 +184,7 @@ export type TSearchProjectsDTO = { name?: string; limit?: number; offset?: number; + type?: ProjectType; options?: { enabled?: boolean }; orderBy?: ProjectIdentityOrderBy; orderDirection?: OrderByDirection; diff --git a/frontend/src/hooks/useNavigationBlocker.tsx b/frontend/src/hooks/useNavigationBlocker.tsx new file mode 100644 index 000000000..e427d6a7d --- /dev/null +++ b/frontend/src/hooks/useNavigationBlocker.tsx @@ -0,0 +1,40 @@ +import { useCallback } from "react"; +import { useBlocker } from "@tanstack/react-router"; + +import { + BatchContext, + useBatchModeActions +} from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.store"; + +type TNavigationBlockerReturn = { + isBlocked: boolean; +}; + +export const useNavigationBlocker = ({ + shouldBlock = false, + message = "Are you sure you want to leave? You may have unsaved changes.", + context +}: { + shouldBlock: boolean; + message: string; + context: BatchContext; +}): TNavigationBlockerReturn => { + const { clearAllPendingChanges } = useBatchModeActions(); + const blockerFn = useCallback(() => { + if (!shouldBlock) return false; + + // eslint-disable-next-line no-alert + const confirmed = window.confirm(message); + if (confirmed) { + clearAllPendingChanges(context); + } + + return !confirmed; + }, [shouldBlock, message, context]); + + useBlocker(blockerFn, shouldBlock); + + return { + isBlocked: shouldBlock + }; +}; diff --git a/frontend/src/layouts/KmsLayout/KmsLayout.tsx b/frontend/src/layouts/KmsLayout/KmsLayout.tsx index 3e671a0e2..dbe1192f7 100644 --- a/frontend/src/layouts/KmsLayout/KmsLayout.tsx +++ b/frontend/src/layouts/KmsLayout/KmsLayout.tsx @@ -1,11 +1,16 @@ +import { faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { Menu, MenuItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; + +import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const KmsLayout = () => { const { currentWorkspace } = useWorkspace(); + const { assumedPrivilegeDetails } = useProjectPermission(); return (
@@ -19,40 +24,106 @@ export const KmsLayout = () => { className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" >
+ {assumedPrivilegeDetails && }
diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx index 0bab1fcb5..7b2e5f154 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx @@ -1,12 +1,12 @@ import { faBook, faCog, - faCubes, faDoorClosed, faInfinity, faMoneyBill, faPlug, faShare, + faTable, faUserCog, faUsers, faUserTie @@ -56,7 +56,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
- +
Projects
diff --git a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx index 00b9f4538..cf9f22580 100644 --- a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx +++ b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx @@ -1,14 +1,27 @@ import { useTranslation } from "react-i18next"; -import { faMobile } from "@fortawesome/free-solid-svg-icons"; +import { + faBell, + faCertificate, + faCog, + faFileLines, + faHome, + faMobile, + faSitemap, + faStamp, + faUsers +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { Menu, MenuItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; + +import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const PkiManagerLayout = () => { const { currentWorkspace } = useWorkspace(); + const { assumedPrivilegeDetails } = useProjectPermission(); const { t } = useTranslation(); return ( @@ -24,70 +37,157 @@ export const PkiManagerLayout = () => { className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" >
+ {assumedPrivilegeDetails && }
diff --git a/frontend/src/layouts/ProjectGeneralLayout/ProjectGeneralLayout.tsx b/frontend/src/layouts/ProjectGeneralLayout/ProjectGeneralLayout.tsx deleted file mode 100644 index f2343eb66..000000000 --- a/frontend/src/layouts/ProjectGeneralLayout/ProjectGeneralLayout.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Link, Outlet } from "@tanstack/react-router"; -import { motion } from "framer-motion"; - -import { Menu, MenuItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; - -export const ProjectGeneralLayout = () => { - const { currentWorkspace } = useWorkspace(); - - return ( -
-
- - - -
- -
-
-
- ); -}; diff --git a/frontend/src/layouts/ProjectGeneralLayout/index.tsx b/frontend/src/layouts/ProjectGeneralLayout/index.tsx deleted file mode 100644 index d048fbebc..000000000 --- a/frontend/src/layouts/ProjectGeneralLayout/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProjectGeneralLayout } from "./ProjectGeneralLayout"; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx deleted file mode 100644 index b239dc17c..000000000 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { faDotCircle, faMobile, faWindowMaximize } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link, Outlet, useLocation } from "@tanstack/react-router"; -import { motion } from "framer-motion"; -import { twMerge } from "tailwind-merge"; - -import { ShouldWrap } from "@app/components/utilities/ShouldWrapComponent"; -import { - Divider, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - Lottie, - Menu, - MenuItem, - Tooltip -} from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; -import { getCurrentProductFromUrl } from "@app/helpers/project"; -import { useLocalStorageState } from "@app/hooks"; -import { ProjectType } from "@app/hooks/api/workspace/types"; - -import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner"; - -enum SidebarStyle { - Expanded = "expanded", - Collapsed = "collapsed", - ExpandOnHover = "expand-on-hover" -} -const MIN_SIDEBAR_SIZE = "55px"; -const MAX_SIDEBAR_SIZE = "220px"; -// This is a generic layout shared by all types of projects. -// If the product layout differs significantly, create a new layout as needed. -export const ProjectLayout = () => { - const location = useLocation(); - const { currentWorkspace } = useWorkspace(); - const [sidebarStyle, setSidebarStyle] = useLocalStorageState( - "project-sidebar-style", - SidebarStyle.ExpandOnHover - ); - - const { t } = useTranslation(); - const { assumedPrivilegeDetails } = useProjectPermission(); - - const minSidebarWidth = - sidebarStyle === SidebarStyle.Expanded ? MAX_SIDEBAR_SIZE : MIN_SIDEBAR_SIZE; - const maxSidebarWidth = - sidebarStyle === SidebarStyle.Collapsed ? MIN_SIDEBAR_SIZE : MAX_SIDEBAR_SIZE; - - const currentProductType = getCurrentProductFromUrl(location.pathname); - const isSecretManager = currentProductType === ProjectType.SecretManager; - const isPki = currentProductType === ProjectType.CertificateManager; - const isKms = currentProductType === ProjectType.KMS; - const isSsh = currentProductType === ProjectType.SSH; - const isSecretScanning = currentProductType === ProjectType.SecretScanning; - - return ( - <> -
-
- - - -
- {assumedPrivilegeDetails && } - -
-
-
-
- -

- {` ${t("common.no-mobile")} `} -

-
- - ); -}; diff --git a/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx b/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx index 52882ba6d..2b4fcff80 100644 --- a/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx +++ b/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx @@ -3,10 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button } from "@app/components/v2"; import { useProjectPermission, useWorkspace } from "@app/context"; -import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project"; +import { getProjectHomePage } from "@app/helpers/project"; import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; -import { ProjectType } from "@app/hooks/api/workspace/types"; export const AssumePrivilegeModeBanner = () => { const { currentWorkspace } = useWorkspace(); @@ -37,10 +36,7 @@ export const AssumePrivilegeModeBanner = () => { }, { onSuccess: () => { - const url = getProjectHomePage( - getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager - ); - + const url = getProjectHomePage(currentWorkspace.type); window.location.href = url.replace("$projectId", currentWorkspace.id); } } diff --git a/frontend/src/layouts/ProjectLayout/components/MenuIconButton/Copy.tsx b/frontend/src/layouts/ProjectLayout/components/MenuIconButton/Copy.tsx deleted file mode 100644 index f7c30c883..000000000 --- a/frontend/src/layouts/ProjectLayout/components/MenuIconButton/Copy.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { ComponentPropsWithRef, ElementType, useRef } from "react"; -import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react"; -import { twMerge } from "tailwind-merge"; - -import { MenuItemProps } from "@app/components/v2"; - -export const MenuIconButton = ({ - children, - icon, - className, - isDisabled, - isSelected, - as: Item = "div", - description, - // wrapping in forward ref with generic component causes the loss of ts definitions on props - inputRef, - lottieIconMode = "forward", - ...props -}: MenuItemProps & - ComponentPropsWithRef & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => { - const iconRef = useRef(null); - return ( -
- iconRef.current?.play()} - onMouseLeave={() => iconRef.current?.stop()} - ref={inputRef} - {...props} - > -
- {icon && ( -
- { - iconRef.current = el; - }} - src={`/lotties/${icon}.json`} - loop - className="h-full w-full" - mode={lottieIconMode} - /> -
- )} -
- {children} -
- -
- ); -}; - -// export const MenuIconButton = ({ -// children, -// icon, -// className, -// isDisabled, -// isSelected, -// as: Item = "div", -// description, -// // wrapping in forward ref with generic component causes the loss of ts definitions on props -// inputRef, -// lottieIconMode = "forward", -// ...props -// }: MenuItemProps & -// ComponentPropsWithRef & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => { -// const iconRef = useRef(null); -// return ( -//
-// iconRef.current?.play()} -// onMouseLeave={() => iconRef.current?.stop()} -// ref={inputRef} -// {...props} -// > -//
-// {icon && ( -//
-// { -// iconRef.current = el; -// }} -// src={`/lotties/${icon}.json`} -// loop -// className="h-full w-full" -// mode={lottieIconMode} -// /> -//
-// )} -//
-// {children} -//
-// -//
-// ); -// }; diff --git a/frontend/src/layouts/ProjectLayout/components/MenuIconButton/MenuIconButton.tsx b/frontend/src/layouts/ProjectLayout/components/MenuIconButton/MenuIconButton.tsx deleted file mode 100644 index bff09423c..000000000 --- a/frontend/src/layouts/ProjectLayout/components/MenuIconButton/MenuIconButton.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { ComponentPropsWithRef, ElementType, useRef } from "react"; -import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react"; -import { twMerge } from "tailwind-merge"; - -import { MenuItemProps } from "@app/components/v2"; - -export const MenuIconButton = ({ - children, - icon, - className, - isDisabled, - isSelected, - as: Item = "div", - description, - // wrapping in forward ref with generic component causes the loss of ts definitions on props - inputRef, - lottieIconMode = "forward", - ...props -}: MenuItemProps & - ComponentPropsWithRef & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => { - const iconRef = useRef(null); - return ( -
- iconRef.current?.play()} - onMouseLeave={() => iconRef.current?.stop()} - ref={inputRef} - {...props} - > -
- {icon && ( -
- { - iconRef.current = el; - }} - src={`/lotties/${icon}.json`} - loop - className="h-full w-full" - mode={lottieIconMode} - /> -
- )} -
- {children} -
- -
- ); -}; diff --git a/frontend/src/layouts/ProjectLayout/components/MenuIconButton/index.tsx b/frontend/src/layouts/ProjectLayout/components/MenuIconButton/index.tsx deleted file mode 100644 index 6655c7091..000000000 --- a/frontend/src/layouts/ProjectLayout/components/MenuIconButton/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MenuIconButton } from "./MenuIconButton"; diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index 80fcee1dc..b66717a5c 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -31,14 +31,13 @@ import { useSubscription, useWorkspace } from "@app/context"; -import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project"; +import { getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { useGetUserWorkspaces } from "@app/hooks/api"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { Workspace } from "@app/hooks/api/workspace/types"; -// TODO(pta): add search to project select export const ProjectSelect = () => { const [searchProject, setSearchProject] = useState(""); const { currentWorkspace } = useWorkspace(); @@ -102,9 +101,7 @@ export const ProjectSelect = () => {
{
- +
{currentWorkspace?.name}
@@ -161,7 +158,7 @@ export const ProjectSelect = () => { // to reproduce change this back to router.push and switch between two projects with different env count // look into this on dashboard revamp const url = linkOptions({ - to: getProjectHomePage(workspace.defaultProduct), + to: getProjectHomePage(workspace.type), params: { projectId: workspace.id } diff --git a/frontend/src/layouts/ProjectLayout/components/SidebarHeader/SidebarHeader.tsx b/frontend/src/layouts/ProjectLayout/components/SidebarHeader/SidebarHeader.tsx deleted file mode 100644 index dc74fdad5..000000000 --- a/frontend/src/layouts/ProjectLayout/components/SidebarHeader/SidebarHeader.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { - faAngleDown, - faArrowLeft, - faArrowUpRightFromSquare, - faCheck -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link, useNavigate } from "@tanstack/react-router"; - -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/v2"; -import { useOrganization, useUser } from "@app/context"; -import { useGetOrganizations, useLogoutUser } from "@app/hooks/api"; -import { AuthMethod } from "@app/hooks/api/users/types"; - -type Prop = { - onChangeOrg: (orgId: string) => void; -}; - -export const SidebarHeader = ({ onChangeOrg }: Prop) => { - const { currentOrg } = useOrganization(); - const { user } = useUser(); - const navigate = useNavigate(); - const { data: orgs } = useGetOrganizations(); - - const logout = useLogoutUser(); - const logOutUser = async () => { - try { - console.log("Logging out..."); - await logout.mutateAsync(); - navigate({ to: "/login" }); - } catch (error) { - console.error(error); - } - }; - - return ( -
- -
- -
- - - -
-
- {currentOrg?.name.charAt(0)} -
-
- {currentOrg?.name} -
- -
-
- -
{user?.username}
- {orgs?.map((org) => { - return ( - - - - ); - })} - -
- - - - - -
- {user?.firstName?.charAt(0)} - {user?.lastName && user?.lastName?.charAt(0)} -
-
- -
{user?.username}
- - Personal Settings - - - - Documentation - - - - - - Join Slack Community - - - - {user?.superAdmin && ( - - - Server Admin Console - - - )} - - - Organization Admin Console - - -
- - - -
- ); -}; diff --git a/frontend/src/layouts/ProjectLayout/components/SidebarHeader/index.tsx b/frontend/src/layouts/ProjectLayout/components/SidebarHeader/index.tsx deleted file mode 100644 index bdc4db6ef..000000000 --- a/frontend/src/layouts/ProjectLayout/components/SidebarHeader/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SidebarHeader } from "./SidebarHeader"; diff --git a/frontend/src/layouts/ProjectLayout/index.tsx b/frontend/src/layouts/ProjectLayout/index.tsx deleted file mode 100644 index ab759db95..000000000 --- a/frontend/src/layouts/ProjectLayout/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProjectLayout } from "./ProjectLayout"; diff --git a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx index ed1857334..7a1eb932a 100644 --- a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx +++ b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx @@ -1,19 +1,31 @@ import { useTranslation } from "react-i18next"; -import { faMobile } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowsSpin, + faCheckToSlot, + faCog, + faHome, + faMobile, + faPuzzlePiece, + faUsers, + faVault +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { Badge, Menu, MenuItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount, useGetSecretRotations } from "@app/hooks/api"; +import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; + export const SecretManagerLayout = () => { const { currentWorkspace } = useWorkspace(); + const { assumedPrivilegeDetails } = useProjectPermission(); const { t } = useTranslation(); const workspaceId = currentWorkspace?.id || ""; @@ -50,73 +62,150 @@ export const SecretManagerLayout = () => { className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" >
+ {assumedPrivilegeDetails && }
diff --git a/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx b/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx index 113d793e5..8c54b6a1e 100644 --- a/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx +++ b/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx @@ -1,11 +1,22 @@ +import { + faCog, + faDatabase, + faHome, + faMagnifyingGlass, + faUsers +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { Menu, MenuItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; + +import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const SecretScanningLayout = () => { const { currentWorkspace } = useWorkspace(); + const { assumedPrivilegeDetails } = useProjectPermission(); return (
@@ -19,40 +30,106 @@ export const SecretScanningLayout = () => { className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" >
+ {assumedPrivilegeDetails && }
diff --git a/frontend/src/layouts/SshLayout/SshLayout.tsx b/frontend/src/layouts/SshLayout/SshLayout.tsx index c62f91ab1..50ce87af9 100644 --- a/frontend/src/layouts/SshLayout/SshLayout.tsx +++ b/frontend/src/layouts/SshLayout/SshLayout.tsx @@ -1,12 +1,22 @@ +import { faCog, faHome, faServer, faStamp, faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Menu, MenuItem } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; + +import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const SshLayout = () => { const { currentWorkspace } = useWorkspace(); + const { assumedPrivilegeDetails } = useProjectPermission(); return (
@@ -20,49 +30,116 @@ export const SshLayout = () => { className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" >
+ {assumedPrivilegeDetails && }
diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx index bca796fc9..366f51c62 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx @@ -82,7 +82,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { }); navigate({ - to: "/projects/$projectId/cert-manager/pki-collections/$collectionId", + to: "/projects/cert-management/$projectId/pki-collections/$collectionId", params: { projectId, collectionId diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx index 1a6e613b6..ff525b1f4 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx @@ -21,7 +21,6 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useListWorkspacePkiCollections } from "@app/hooks/api"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -61,7 +60,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => { key={`pki-collection-${pkiCollection.id}`} onClick={() => navigate({ - to: `/projects/$projectId/${ProjectType.CertificateManager}/pki-collections/$collectionId` as const, + to: "/projects/cert-management/$projectId/pki-collections/$collectionId", params: { projectId, collectionId: pkiCollection.id diff --git a/frontend/src/pages/cert-manager/AlertingPage/route.tsx b/frontend/src/pages/cert-manager/AlertingPage/route.tsx index 0b402699f..89f3b1888 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/route.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { AlertingPage } from "./AlertingPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting" )({ component: AlertingPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index 17a9fb8bf..0bf3c4431 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -18,7 +18,6 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api"; import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal"; @@ -71,7 +70,7 @@ const Page = () => { handlePopUpClose("deleteCa"); navigate({ - to: `/projects/$projectId/${ProjectType.CertificateManager}/certificate-authorities` as const, + to: "/projects/cert-management/$projectId/certificate-authorities", params: { projectId } diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx index 8a374589e..bbe3e51d7 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName" )({ component: CertAuthDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Certificate Authorities", link: linkOptions({ - to: "/projects/$projectId/cert-manager/certificate-authorities", + to: "/projects/cert-management/$projectId/certificate-authorities", params: { projectId: params.projectId } diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx index e0b91a239..00ab03625 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx @@ -30,7 +30,6 @@ import { getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -79,7 +78,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { key={`ca-${ca.id}`} onClick={() => navigate({ - to: `/projects/$projectId/${ProjectType.CertificateManager}/ca/$caName` as const, + to: "/projects/cert-management/$projectId/ca/$caName", params: { projectId: currentWorkspace.id, caName: ca.name diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx index 36baa6e9e..b01369c07 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities" )({ component: CertificateAuthoritiesPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/route.tsx b/frontend/src/pages/cert-manager/CertificatesPage/route.tsx index 7039f8770..68deb41b9 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/route.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { CertificatesPage } from "./CertificatesPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates" )({ component: CertificatesPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index 33c4808fd..0261d55fd 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -19,7 +19,6 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api"; import { PkiItemType } from "@app/hooks/api/pkiCollections/constants"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { PkiCollectionModal } from "../AlertingPage/components/PkiCollectionModal"; @@ -57,7 +56,7 @@ export const PkiCollectionPage = () => { }); handlePopUpClose("deletePkiCollection"); navigate({ - to: `/projects/$projectId/${ProjectType.CertificateManager}/certificates` as const, + to: "/projects/cert-management/$projectId/certificates", params: { projectId } diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx index ac29245e0..7fef38221 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx @@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId" )({ component: PkiCollectionDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Certificate Collections", link: linkOptions({ - to: "/projects/$projectId/cert-manager/certificates", + to: "/projects/cert-management/$projectId/certificates", params: { projectId: params.projectId } diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx index 6f7e1362c..04db9861b 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -22,7 +22,6 @@ import { useWorkspace } from "@app/context"; import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal"; @@ -61,7 +60,7 @@ const Page = () => { handlePopUpClose("deletePkiSubscriber"); navigate({ - to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers` as const, + to: "/projects/cert-management/$projectId/subscribers", params: { projectId } diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx index e9b0c7e7a..429a10574 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName" )({ component: PkiSubscriberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Subscribers", link: linkOptions({ - to: "/projects/$projectId/cert-manager/subscribers", + to: "/projects/cert-management/$projectId/subscribers", params: { projectId: params.projectId } diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx index eead0371a..d649c43a7 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx @@ -38,7 +38,6 @@ import { PkiSubscriberStatus, pkiSubscriberStatusToNameMap } from "@app/hooks/api/pkiSubscriber/constants"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -76,7 +75,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { key={`pki-subscriber-${subscriber.id}`} onClick={() => navigate({ - to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers/$subscriberName` as const, + to: "/projects/cert-management/$projectId/subscribers/$subscriberName", params: { projectId: currentWorkspace.id, subscriberName: subscriber.name diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx index 58f906686..c8a2541d8 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { PkiSubscribersPage } from "./PkiSubscribersPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/" )({ component: PkiSubscribersPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx index bc0fb5e04..cf568bf2c 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { PkiTemplateListPage } from "./PkiTemplateListPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-templates/" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/" )({ component: PkiTemplateListPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx b/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx index 6c42e66dc..22c1a6961 100644 --- a/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx @@ -2,12 +2,13 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; const tabs = [ { name: "General", key: "tab-project-general", - Component: () =>
Coming soon
+ Component: ProjectGeneralTab } ]; diff --git a/frontend/src/pages/cert-manager/SettingsPage/route.tsx b/frontend/src/pages/cert-manager/SettingsPage/route.tsx index dc8922092..f1400f30e 100644 --- a/frontend/src/pages/cert-manager/SettingsPage/route.tsx +++ b/frontend/src/pages/cert-manager/SettingsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { SettingsPage } from "./SettingsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/settings" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings" )({ component: SettingsPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/layout.tsx b/frontend/src/pages/cert-manager/layout.tsx index 48f3a88b3..6b846909e 100644 --- a/frontend/src/pages/cert-manager/layout.tsx +++ b/frontend/src/pages/cert-manager/layout.tsx @@ -1,9 +1,37 @@ import { createFileRoute } from "@tanstack/react-router"; +import { BreadcrumbTypes } from "@app/components/v2"; +import { workspaceKeys } from "@app/hooks/api"; +import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; +import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout"; +import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout" + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" )({ - component: PkiManagerLayout + component: PkiManagerLayout, + beforeLoad: async ({ params, context }) => { + const project = await context.queryClient.ensureQueryData({ + queryKey: workspaceKeys.getWorkspaceById(params.projectId), + queryFn: () => fetchWorkspaceById(params.projectId) + }); + + await context.queryClient.ensureQueryData({ + queryKey: roleQueryKeys.getUserProjectPermissions({ + workspaceId: params.projectId + }), + queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + }); + + return { + project, + breadcrumbs: [ + { + type: BreadcrumbTypes.Component, + component: ProjectSelect + } + ] + }; + } }); diff --git a/frontend/src/pages/kms/KmipPage/route.tsx b/frontend/src/pages/kms/KmipPage/route.tsx index 520cae116..662bfc32c 100644 --- a/frontend/src/pages/kms/KmipPage/route.tsx +++ b/frontend/src/pages/kms/KmipPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { KmipPage } from "./KmipPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/kmip" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip" )({ component: KmipPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/kms/OverviewPage/route.tsx b/frontend/src/pages/kms/OverviewPage/route.tsx index b758782d7..8d4270a25 100644 --- a/frontend/src/pages/kms/OverviewPage/route.tsx +++ b/frontend/src/pages/kms/OverviewPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { OverviewPage } from "./OverviewPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/overview" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview" )({ component: OverviewPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx b/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx index 566107a77..cdc0582dc 100644 --- a/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx @@ -2,12 +2,13 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; const tabs = [ { name: "General", key: "tab-project-general", - Component: () =>
Coming soon...
+ Component: ProjectGeneralTab } ]; diff --git a/frontend/src/pages/kms/SettingsPage/route.tsx b/frontend/src/pages/kms/SettingsPage/route.tsx index 8dd90bbdd..b47df3f86 100644 --- a/frontend/src/pages/kms/SettingsPage/route.tsx +++ b/frontend/src/pages/kms/SettingsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { SettingsPage } from "./SettingsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/settings" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings" )({ component: SettingsPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/kms/layout.tsx b/frontend/src/pages/kms/layout.tsx index 48cdca4af..60bc35ab7 100644 --- a/frontend/src/pages/kms/layout.tsx +++ b/frontend/src/pages/kms/layout.tsx @@ -1,9 +1,37 @@ import { createFileRoute } from "@tanstack/react-router"; +import { BreadcrumbTypes } from "@app/components/v2"; +import { workspaceKeys } from "@app/hooks/api"; +import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; +import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; import { KmsLayout } from "@app/layouts/KmsLayout"; +import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout" )({ - component: KmsLayout + component: KmsLayout, + beforeLoad: async ({ params, context }) => { + const project = await context.queryClient.ensureQueryData({ + queryKey: workspaceKeys.getWorkspaceById(params.projectId), + queryFn: () => fetchWorkspaceById(params.projectId) + }); + + await context.queryClient.ensureQueryData({ + queryKey: roleQueryKeys.getUserProjectPermissions({ + workspaceId: params.projectId + }), + queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + }); + + return { + project, + breadcrumbs: [ + { + type: BreadcrumbTypes.Component, + component: ProjectSelect + } + ] + }; + } }); diff --git a/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx index 674018f14..5a9cac8bb 100644 --- a/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx +++ b/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -105,7 +105,7 @@ export const OrgAdminProjects = withPermission( {isProjectsLoading && } {!isProjectsLoading && - projects?.map(({ name, slug, createdAt, id, defaultProduct }) => ( + projects?.map(({ name, slug, createdAt, id, type }) => ( {name} {slug} @@ -126,7 +126,7 @@ export const OrgAdminProjects = withPermission( onClick={(e) => { e.stopPropagation(); e.preventDefault(); - handleAccessProject(defaultProduct, id); + handleAccessProject(type, id); }} icon={} disabled={ diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 45a774780..b1181a68b 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -33,6 +33,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { OCIConnectionForm } from "./OCIConnectionForm"; +import { OktaConnectionForm } from "./OktaConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { RailwayConnectionForm } from "./RailwayConnectionForm"; @@ -149,6 +150,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.Okta: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -253,6 +256,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.Okta: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx index 70128025d..e7d9cf80c 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx @@ -6,7 +6,11 @@ import { slugSchema } from "@app/lib/schemas"; export const genericAppConnectionFieldsSchema = z.object({ name: slugSchema({ min: 1, max: 64, field: "Name" }), - description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish() + description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish(), + gatewayId: z + .string() + .nullish() + .transform((v) => (v === "" ? null : v)) }); export const GenericAppConnectionsFields = () => { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx index f7d48744d..0735a863c 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx @@ -1,10 +1,17 @@ import { useState } from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; -import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { + OrgGatewayPermissionActions, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { gatewaysQueryKeys } from "@app/hooks/api"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { MsSqlConnectionMethod, @@ -51,6 +58,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { defaultValues: appConnection ?? { app: AppConnection.MsSql, method: MsSqlConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { host: "", port: 1433, @@ -71,6 +79,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -90,6 +99,55 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
{ defaultValues: appConnection ?? { app: AppConnection.MySql, method: MySqlConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { host: "", port: 3306, @@ -68,6 +76,7 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -87,6 +96,55 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Okta) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(OktaConnectionMethod.ApiToken), + credentials: z.object({ + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .min(1, "Instance URL required") + .max(255), + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .regex(/^00[a-zA-Z0-9_-]{40}$/, "Invalid Okta API Token format") + }) + }) +]); + +type FormData = z.infer; + +export const OktaConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Okta, + method: OktaConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OracleDBConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OracleDBConnectionForm.tsx index 25dded675..87d4fe07b 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OracleDBConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OracleDBConnectionForm.tsx @@ -1,10 +1,17 @@ import { useState } from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; -import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { + OrgGatewayPermissionActions, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { gatewaysQueryKeys } from "@app/hooks/api"; import { OracleDBConnectionMethod, TOracleDBConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal"; @@ -48,6 +55,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { defaultValues: appConnection ?? { app: AppConnection.OracleDB, method: OracleDBConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { host: "", port: 1521, @@ -68,6 +76,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -87,6 +96,55 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
{ defaultValues: appConnection ?? { app: AppConnection.Postgres, method: PostgresConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { host: "", port: 5432, @@ -68,6 +76,7 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -87,6 +96,55 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
{ title="Audit logs" description="Audit logs for security and compliance teams to monitor information access." /> - +
diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx index 87957ad22..71d418c9d 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { format } from "date-fns"; import ms from "ms"; import { twMerge } from "tailwind-merge"; @@ -18,6 +17,7 @@ import { Select, SelectItem } from "@app/components/v2"; +import { formatDateTime, Timezone } from "@app/helpers/datetime"; import { auditLogDateFilterFormSchema, @@ -28,9 +28,19 @@ import { type Props = { setFilter: (data: TAuditLogDateFilterFormData) => void; filter: TAuditLogDateFilterFormData; + setTimezone: (timezone: Timezone) => void; + timezone: Timezone; }; const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"]; -export const LogsDateFilter = ({ setFilter, filter }: Props) => { + +const RELATIVE_OPTIONS = [ + { label: "Minutes", unit: "m", values: [5, 10, 15, 30, 45] }, + { label: "Hours", unit: "h", values: [1, 2, 3, 6, 8, 12] }, + { label: "Days", unit: "d", values: [1, 2, 3, 4, 5, 6] }, + { label: "Weeks", unit: "w", values: [1, 2, 3, 4] } +]; + +export const LogsDateFilter = ({ setFilter, filter, timezone, setTimezone }: Props) => { const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); const [isPopupOpen, setIsPopOpen] = useState(false); @@ -59,154 +69,258 @@ export const LogsDateFilter = ({ setFilter, filter }: Props) => { }; return ( - setIsPopOpen(el)}> -
- {filter.type === AuditLogDateFilterType.Relative ? ( - <> - {RELATIVE_VALUES.map((el) => ( - - ))} - - ) : ( - <> -
- {format(filter.startDate, "yyyy-MM-dd HH:mm")} -
-
- -
-
- {format(filter.endDate, "yyyy-MM-dd HH:mm")} -
- - )} - - - -
- -
- ( - - - - )} - /> - {selectType === AuditLogDateFilterType.Relative && ( + {el} + + ))} + + ) : ( +
+
+ {formatDateTime({ + timezone, + timestamp: filter.startDate, + dateFormat: "yyyy/MM/dd HH:mm" + })} +
+
+ +
+
+ {formatDateTime({ + timezone, + timestamp: filter.endDate, + dateFormat: "yyyy/MM/dd HH:mm" + })} +
+
+ )} + + + +
+ + ( - - - +
+ + +
)} /> - )} - {selectType === AuditLogDateFilterType.Absolute && ( -
+ {selectType === AuditLogDateFilterType.Relative && ( { + name="relativeModeValue" + render={({ field }) => { + const duration = field.value?.substring(0, field.value.length - 1); + const unitOfTime = field.value?.at(-1); return ( - - - +
+ {RELATIVE_OPTIONS.map(({ label, unit, values }) => ( +
+
{label}
+ {values.map((v) => { + const value = `${v}${unit}`; + return ( + + ); + })} +
+ ))} +
+ + field.onChange(`${val}${unitOfTime}`)} + max={60} + min={1} + /> + + + + +
+
); }} /> -
-
- + )} + {selectType === AuditLogDateFilterType.Absolute && ( +
+ { + return ( + + + + ); + }} + /> + + { + return ( + + + + ); + }} + />
- { - return ( - - - - ); - }} - /> + )} +
+
- )} -
- -
- - - + + + + + ); }; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 73cc76ed7..572306973 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -300,10 +300,10 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => { onChange(e); }} placeholder="All projects" - options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({ + options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, - type: defaultProduct + type }))} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx index 7f676121f..d4d74659f 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx @@ -1,8 +1,11 @@ import { useEffect, useState } from "react"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ms from "ms"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { Timezone } from "@app/helpers/datetime"; import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -20,10 +23,11 @@ type Props = { presets?: Presets; refetchInterval?: number; showFilters?: boolean; + pageView?: boolean; }; export const LogsSection = withPermission( - ({ presets, refetchInterval, showFilters = true }: Props) => { + ({ presets, refetchInterval, showFilters = true, pageView = false }: Props) => { const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -31,6 +35,8 @@ export const LogsSection = withPermission( eventType: presets?.eventType || [], actor: presets?.actorId }); + const [timezone, setTimezone] = useState(Timezone.Local); + const [dateFilter, setDateFilter] = useState({ startDate: new Date(Number(new Date()) - ms("1h")), endDate: new Date(), @@ -43,10 +49,85 @@ export const LogsSection = withPermission( handlePopUpOpen("upgradePlan"); } }, [subscription]); + + if (pageView) + return ( +
+
+
+
+

Audit History

+ +
+ + Docs + +
+
+
+
+
+ {showFilters && ( + + )} + {showFilters && ( + + )} +
+
+
+ + { + handlePopUpToggle("upgradePlan", isOpen); + }} + text="You can use audit logs if you switch to a paid Infisical plan." + /> +
+
+ ); + return (
- {showFilters && } + {showFilters && ( + + )} {showFilters && ( )} @@ -67,6 +148,7 @@ export const LogsSection = withPermission( environment: logFilter?.environment?.slug, actor: logFilter?.actor }} + timezone={timezone} /> { +export const LogsTable = ({ filter, refetchInterval, timezone }: Props) => { // Determine the project ID for filtering const filterProjectId = // Use the projectId from the filter if it exists @@ -57,16 +57,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { - - Timestamp - - - - + Timestamp Event @@ -79,6 +70,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { rowNumber={index + i * AUDIT_LOG_LIMIT + 1} auditLog={auditLog} key={`audit-log-${auditLog.id}`} + timezone={timezone} /> ))} @@ -96,7 +88,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { {!isEmpty && ( -
- )} -
+
+
+
-
- {workspace.description} +
+

{workspace.name}

+

+ {getProjectTitle(workspace.type)}{" "} + {workspace.description ? `- ${workspace.description}` : ""} +

+ {workspace.isMember ? ( + + + Joined + + ) : ( +
+ +
+ )}
))}
diff --git a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx index f9d2ed470..ea0d1d2ff 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx @@ -2,9 +2,9 @@ import { ReactNode, useMemo, useState } from "react"; import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; import { faArrowDownAZ, - faArrowRight, faArrowUpZA, faBorderAll, + faCheckCircle, faList, faMagnifyingGlass, faPlus, @@ -13,12 +13,26 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + IconButton, + Input, + Lottie, + Pagination, + Skeleton, + Tooltip +} from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; -import { getProjectHomePage } from "@app/helpers/project"; +import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project"; import { getUserTablePreference, PreferenceKey, @@ -29,7 +43,7 @@ import { useGetUserWorkspaces } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; type Props = { onAddNewProject: () => void; @@ -53,6 +67,9 @@ export const MyProjectView = ({ }: Props) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); + const [projectTypeFilter, setProjectTypeFilter] = useState>>( + {} + ); const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces(); const { @@ -67,6 +84,7 @@ export const MyProjectView = ({ } = usePagination(ProjectOrderBy.Name, { initPerPage: getUserTablePreference("myProjectsTable", PreferenceKey.PerPage, 24) }); + const isTableFilteredByType = Boolean(Object.values(projectTypeFilter).some((el) => el)); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); @@ -88,13 +106,18 @@ export const MyProjectView = ({ const filteredWorkspaces = useMemo( () => workspaces - .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) + .filter((ws) => { + if (isTableFilteredByType && !projectTypeFilter?.[ws.type]) { + return false; + } + return ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()); + }) .sort((a, b) => orderDirection === OrderByDirection.ASC ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) : b.name.toLowerCase().localeCompare(a.name.toLowerCase()) ), - [searchFilter, orderDirection, workspaces] + [searchFilter, orderDirection, workspaces, projectTypeFilter] ); useResetPageHelper({ @@ -117,6 +140,15 @@ export const MyProjectView = ({ }; }, [filteredWorkspaces, projectFavorites, offset, limit, page]); + const handleToggleFilterByProjectType = (type: ProjectType) => { + setProjectTypeFilter((state) => { + return { + ...(state || {}), + [type]: !state?.[type] + }; + }); + }; + const addProjectToFavorites = async (projectId: string) => { try { if (currentOrg?.id) { @@ -153,61 +185,53 @@ export const MyProjectView = ({
{ navigate({ - to: getProjectHomePage(workspace.defaultProduct), + to: getProjectHomePage(workspace.type), params: { projectId: workspace.id } }); }} key={workspace.id} - className="flex h-40 min-w-72 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" + className="cursor-pointer overflow-clip rounded border border-l-[4px] border-mineshaft-600 border-l-mineshaft-400 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:border-l-primary hover:bg-mineshaft-700" > -
-
{workspace.name}
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} +
+
+ - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
- -
- {workspace.description} -
- -
- + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> + )} +
+

+ {workspace.description || "No description"} +

); const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( @@ -215,21 +239,33 @@ export const MyProjectView = ({
{ navigate({ - to: getProjectHomePage(workspace.defaultProduct), + to: getProjectHomePage(workspace.type), params: { projectId: workspace.id } }); }} key={workspace.id} - className={`group grid h-14 min-w-72 cursor-pointer grid-cols-6 border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ + className={`group flex min-w-72 cursor-pointer border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 hover:bg-mineshaft-700 ${ index === 0 && "rounded-t-md" }`} > -
-
{workspace.name}
+
+
+ +
+
+

{workspace.name}

+

+ {getProjectTitle(workspace.type)}{" "} + {workspace.description ? `- ${workspace.description}` : ""} +

+
-
+
{isFavorite ? ( +
{isProjectViewLoading && Array.apply(0, Array(3)).map((_x, i) => (
No projects match search...
); + } else if (filteredWorkspaces.length === 0 && isTableFilteredByType) { + projectsComponents = ( +
+ +
No projects match filters...
+
+ ); } + return (
@@ -351,6 +398,49 @@ export const MyProjectView = ({
+ + +
+ + + + + +
+
+ + Filter By Project Type + {Object.values(ProjectType).map((el) => ( + { + e.preventDefault(); + handleToggleFilterByProjectType(el); + }} + icon={projectTypeFilter?.[el] && } + iconPos="right" + > +
+ {getProjectTitle(el)} +
+
+ ))} +
+
( - +
)} diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx index 604a37d6d..bb3a03fc1 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx @@ -1,5 +1,6 @@ -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { twMerge } from "tailwind-merge"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -7,21 +8,25 @@ import { Button, FormControl, Input, + Lottie, Modal, ModalClose, ModalContent, TextArea } from "@app/components/v2"; +import { getProjectLottieIcon } from "@app/helpers/project"; import { TProjectTemplate, useCreateProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ name: slugSchema({ min: 1, max: 64, field: "Name" }), - description: z.string().max(500).optional() + description: z.string().max(500).optional(), + type: z.nativeEnum(ProjectType).optional() }); export type FormData = z.infer; @@ -38,6 +43,29 @@ type FormProps = { onComplete: (template: TProjectTemplate) => void; }; +const PROJECT_TYPE_MENU_ITEMS = [ + { + label: "Secrets Manager", + value: ProjectType.SecretManager + }, + { + label: "Certificates Manager", + value: ProjectType.CertificateManager + }, + { + label: "KMS", + value: ProjectType.KMS + }, + { + label: "SSH", + value: ProjectType.SSH + }, + { + label: "Secret Scanning", + value: ProjectType.SecretScanning + } +]; + const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { const createProjectTemplate = useCreateProjectTemplate(); const updateProjectTemplate = useUpdateProjectTemplate(); @@ -45,12 +73,14 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { const { handleSubmit, register, + control, formState: { isSubmitting, errors } } = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: projectTemplate?.name, - description: projectTemplate?.description + description: projectTemplate?.description, + type: ProjectType.SecretManager } }); @@ -90,6 +120,42 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { > + ( + +
+ {PROJECT_TYPE_MENU_ITEMS.map((el) => ( +
field.onChange(el.value)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") { + field.onChange(el.value); + } + }} + > + +
{el.label}
+
+ ))} +
+
+ )} + /> { Name + Type Roles - Environments @@ -80,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { /> )} {filteredTemplates.map((template) => { - const { id, name, roles, environments = [], description } = template; + const { id, name, roles, description, type } = template; return ( onEdit(template)} @@ -99,6 +100,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { )} + {getProjectTitle(type)} {roles.length} {roles.length > 0 && ( @@ -119,26 +121,6 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { )} - - {environments?.length || 0} - {environments?.length && ( - - {environments - ?.sort((a, b) => (a.position > b.position ? 1 : -1)) - .map((env) =>
  • {env.name}
  • )} - - } - > - -
    - )} - {name !== "default" && ( { if (isAccessible) { navigate({ - to: "/projects/$projectId/access-management", + to: `${getProjectBaseURL(project.type)}/access-management` as const, params: { projectId: project.id }, diff --git a/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx b/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx index 096514b69..8bb007e88 100644 --- a/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx +++ b/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx @@ -4,6 +4,8 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectAccessControlTabs } from "@app/types/project"; import { @@ -24,7 +26,7 @@ const Page = () => { const updateSelectedTab = (tab: string) => { navigate({ - to: "/projects/$projectId/access-management", + to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const, search: (prev) => ({ ...prev, selectedTab: tab }), params: { projectId: currentWorkspace.id @@ -32,6 +34,8 @@ const Page = () => { }); }; + const isSecretManager = currentWorkspace.type === ProjectType.SecretManager; + return (
    @@ -48,7 +52,9 @@ const Page = () => {

    Machine Identities

    - Service Tokens + {isSecretManager && ( + Service Tokens + )} Project Roles @@ -60,9 +66,11 @@ const Page = () => { - - - + {isSecretManager && ( + + + + )} diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 37816ceb9..9bd98084e 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -33,6 +33,7 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { getUserTablePreference, PreferenceKey, @@ -158,7 +159,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { onKeyDown={(evt) => { if (evt.key === "Enter") { navigate({ - to: "/projects/$projectId/groups/$groupId", + to: `${getProjectBaseURL(currentWorkspace.type)}/groups/$groupId` as const, params: { projectId: currentWorkspace.id, groupId: id @@ -168,7 +169,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { }} onClick={() => navigate({ - to: "/projects/$projectId/groups/$groupId", + to: `${getProjectBaseURL(currentWorkspace.type)}/groups/$groupId` as const, params: { projectId: currentWorkspace.id, groupId: id diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx index 52ac3b349..5b6e773db 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx @@ -46,6 +46,7 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { formatProjectRoleName } from "@app/helpers/roles"; import { getUserTablePreference, @@ -260,7 +261,7 @@ export const IdentityTab = withProjectPermission( onKeyDown={(evt) => { if (evt.key === "Enter") { navigate({ - to: "/projects/$projectId/identities/$identityId", + to: `${getProjectBaseURL(currentWorkspace.type)}/identities/$identityId` as const, params: { projectId: currentWorkspace.id, identityId: id @@ -270,7 +271,7 @@ export const IdentityTab = withProjectPermission( }} onClick={() => navigate({ - to: "/projects/$projectId/identities/$identityId", + to: `${getProjectBaseURL(currentWorkspace.type)}/identities/$identityId` as const, params: { projectId: currentWorkspace.id, identityId: id diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx index ba4e0fac4..735257293 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx @@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router"; import { Alert, AlertDescription } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { TWorkspaceUser } from "@app/hooks/api/types"; import { MemberRbacSection } from "./MemberRbacSection"; @@ -21,7 +22,7 @@ export const MemberRoleForm = ({ projectMember, onOpenUpgradeModal }: Props) => > { onKeyDown={(evt) => { if (evt.key === "Enter") { navigate({ - to: "/projects/$projectId/members/$membershipId", + to: `${getProjectBaseURL(currentWorkspace.type)}/members/$membershipId`, params: { projectId: workspaceId, membershipId @@ -321,7 +322,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { }} onClick={() => navigate({ - to: "/projects/$projectId/members/$membershipId", + to: `${getProjectBaseURL(currentWorkspace.type)}/members/$membershipId`, params: { projectId: workspaceId, membershipId diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 2a01764b0..211f1d0e8 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -40,6 +40,7 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { isCustomProjectRole } from "@app/helpers/roles"; import { getUserTablePreference, @@ -249,7 +250,7 @@ export const ProjectRoleList = () => { className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" onClick={() => navigate({ - to: "/projects/$projectId/roles/$roleSlug", + to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug`, params: { projectId: currentWorkspace.id, roleSlug: slug @@ -291,7 +292,7 @@ export const ProjectRoleList = () => { onClick={(e) => { e.stopPropagation(); navigate({ - to: "/projects/$projectId/roles/$roleSlug", + to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug`, params: { projectId: currentWorkspace.id, roleSlug: slug diff --git a/frontend/src/pages/project/AccessControlPage/route-cert-manager.tsx b/frontend/src/pages/project/AccessControlPage/route-cert-manager.tsx new file mode 100644 index 000000000..e5549b5be --- /dev/null +++ b/frontend/src/pages/project/AccessControlPage/route-cert-manager.tsx @@ -0,0 +1,32 @@ +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { AccessControlPage } from "./AccessControlPage"; + +const AccessControlPageQuerySchema = z.object({ + selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member), + requesterEmail: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management" +)({ + component: AccessControlPage, + validateSearch: zodValidator(AccessControlPageQuerySchema), + search: { + middlewares: [stripSearchParams({ requesterEmail: "" })] + }, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AccessControlPage/route-kms.tsx b/frontend/src/pages/project/AccessControlPage/route-kms.tsx new file mode 100644 index 000000000..0968565c4 --- /dev/null +++ b/frontend/src/pages/project/AccessControlPage/route-kms.tsx @@ -0,0 +1,32 @@ +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { AccessControlPage } from "./AccessControlPage"; + +const AccessControlPageQuerySchema = z.object({ + selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member), + requesterEmail: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management" +)({ + component: AccessControlPage, + validateSearch: zodValidator(AccessControlPageQuerySchema), + search: { + middlewares: [stripSearchParams({ requesterEmail: "" })] + }, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AccessControlPage/route-secret-manager.tsx b/frontend/src/pages/project/AccessControlPage/route-secret-manager.tsx new file mode 100644 index 000000000..efdb9f9b2 --- /dev/null +++ b/frontend/src/pages/project/AccessControlPage/route-secret-manager.tsx @@ -0,0 +1,32 @@ +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { AccessControlPage } from "./AccessControlPage"; + +const AccessControlPageQuerySchema = z.object({ + selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member), + requesterEmail: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management" +)({ + component: AccessControlPage, + validateSearch: zodValidator(AccessControlPageQuerySchema), + search: { + middlewares: [stripSearchParams({ requesterEmail: "" })] + }, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AccessControlPage/route.tsx b/frontend/src/pages/project/AccessControlPage/route-secret-scanning.tsx similarity index 86% rename from frontend/src/pages/project/AccessControlPage/route.tsx rename to frontend/src/pages/project/AccessControlPage/route-secret-scanning.tsx index 4b51fdecb..2305a761c 100644 --- a/frontend/src/pages/project/AccessControlPage/route.tsx +++ b/frontend/src/pages/project/AccessControlPage/route-secret-scanning.tsx @@ -12,7 +12,7 @@ const AccessControlPageQuerySchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/access-management" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management" )({ component: AccessControlPage, validateSearch: zodValidator(AccessControlPageQuerySchema), diff --git a/frontend/src/pages/project/AccessControlPage/route-ssh.tsx b/frontend/src/pages/project/AccessControlPage/route-ssh.tsx new file mode 100644 index 000000000..d4cc6f11e --- /dev/null +++ b/frontend/src/pages/project/AccessControlPage/route-ssh.tsx @@ -0,0 +1,32 @@ +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { AccessControlPage } from "./AccessControlPage"; + +const AccessControlPageQuerySchema = z.object({ + selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member), + requesterEmail: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management" +)({ + component: AccessControlPage, + validateSearch: zodValidator(AccessControlPageQuerySchema), + search: { + middlewares: [stripSearchParams({ requesterEmail: "" })] + }, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx index b23cfdad1..90c327067 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx @@ -15,6 +15,7 @@ import { } from "@app/components/v2"; import { CopyButton } from "@app/components/v2/CopyButton"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { useDeleteGroupFromWorkspace } from "@app/hooks/api"; import { TGroupMembership } from "@app/hooks/api/groups/types"; @@ -46,7 +47,7 @@ export const GroupDetailsSection = ({ groupMembership }: Props) => { }); navigate({ - to: "/projects/$projectId/access-management", + to: `${getProjectBaseURL(currentWorkspace.type)}/access-management`, params: { projectId: currentWorkspace.id }, diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx index 681deea42..bcffb428f 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -24,7 +24,7 @@ import { Tr } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project"; +import { getProjectHomePage } from "@app/helpers/project"; import { getUserTablePreference, PreferenceKey, @@ -36,7 +36,6 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useListProjectGroupUsers } from "@app/hooks/api/groups/queries"; import { EFilterReturnedUsers, TGroupMembership } from "@app/hooks/api/groups/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { GroupMembershipRow } from "./GroupMembershipRow"; @@ -137,9 +136,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => { text: "User privilege assumption has started" }); - const url = getProjectHomePage( - getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager - ); + const url = getProjectHomePage(currentWorkspace.type); window.location.href = url.replace("$projectId", currentWorkspace.id); } } diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx new file mode 100644 index 000000000..cbf6e0072 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/cert-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Groups + } + }) + }, + { + label: "Group" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx similarity index 79% rename from frontend/src/pages/project/GroupDetailsByIDPage/route.tsx rename to frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx index 684e9fd87..c0dd210c1 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/groups/$groupId" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/groups/$groupId" )({ component: GroupDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,7 +15,7 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/$projectId/access-management", + to: "/projects/kms/$projectId/access-management", params: { projectId: params.projectId }, diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx new file mode 100644 index 000000000..01349a83b --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Groups + } + }) + }, + { + label: "Group" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx new file mode 100644 index 000000000..ed86d0b65 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-scanning/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Groups + } + }) + }, + { + label: "Group" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..17427d706 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/ssh/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Groups + } + }) + }, + { + label: "Group" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index bc4ae078f..19d6c4acc 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -20,7 +20,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context"; -import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project"; +import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { useAssumeProjectPrivileges, @@ -28,7 +28,6 @@ import { useGetWorkspaceIdentityMembershipDetails } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection"; import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection"; @@ -68,9 +67,7 @@ const Page = () => { type: "success", text: "Identity privilege assumption has started" }); - const url = getProjectHomePage( - getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager - ); + const url = getProjectHomePage(currentWorkspace.type); window.location.href = url.replace("$projectId", currentWorkspace.id); } } @@ -89,7 +86,7 @@ const Page = () => { }); handlePopUpClose("deleteIdentity"); navigate({ - to: "/projects/$projectId/access-management", + to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const, params: { projectId: workspaceId }, diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index 3ba76475f..fe574a32d 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -225,7 +225,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ > Save - +
    diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-cert-manager.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-cert-manager.tsx new file mode 100644 index 000000000..d6ef9aa73 --- /dev/null +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-cert-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/identities/$identityId" +)({ + component: IdentityDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/cert-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Identities + } + }) + }, + { + label: "Machine Identity" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx similarity index 79% rename from frontend/src/pages/project/IdentityDetailsByIDPage/route.tsx rename to frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx index 26bd99210..f4fb109e3 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,7 +15,7 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/$projectId/access-management", + to: "/projects/kms/$projectId/access-management", params: { projectId: params.projectId }, diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx new file mode 100644 index 000000000..355428940 --- /dev/null +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId" +)({ + component: IdentityDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Identities + } + }) + }, + { + label: "Machine Identity" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx new file mode 100644 index 000000000..c5491bf72 --- /dev/null +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId" +)({ + component: IdentityDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-scanning/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Identities + } + }) + }, + { + label: "Machine Identity" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..2a82c371f --- /dev/null +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/identities/$identityId" +)({ + component: IdentityDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/ssh/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Identities + } + }) + }, + { + label: "Machine Identity" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index f84e0ea36..7bac32f75 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -21,7 +21,7 @@ import { useOrganization, useWorkspace } from "@app/context"; -import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project"; +import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { useAssumeProjectPrivileges, @@ -29,7 +29,6 @@ import { useGetWorkspaceUserDetails } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection"; import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection"; @@ -73,9 +72,7 @@ export const Page = () => { text: "User privilege assumption has started" }); - const url = getProjectHomePage( - getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager - ); + const url = getProjectHomePage(currentWorkspace.type); window.location.href = url.replace("$projectId", currentWorkspace.id); } } @@ -96,7 +93,7 @@ export const Page = () => { type: "success" }); navigate({ - to: "/projects/$projectId/access-management", + to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const, params: { projectId: currentWorkspace.id } diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 92bdfc043..9817aefdb 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -221,7 +221,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ > Save - +
    diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-cert-manager.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-cert-manager.tsx new file mode 100644 index 000000000..4946c2fe6 --- /dev/null +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-cert-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/members/$membershipId" +)({ + component: MemberDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/cert-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Member + } + }) + }, + { + label: "User" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx similarity index 79% rename from frontend/src/pages/project/MemberDetailsByIDPage/route.tsx rename to frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx index 2436c762f..6628de005 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/route.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId" )({ component: MemberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,7 +15,7 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/$projectId/access-management", + to: "/projects/kms/$projectId/access-management", params: { projectId: params.projectId }, diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx new file mode 100644 index 000000000..2114e5b40 --- /dev/null +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId" +)({ + component: MemberDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Member + } + }) + }, + { + label: "User" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx new file mode 100644 index 000000000..ff66305fd --- /dev/null +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId" +)({ + component: MemberDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-scanning/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Member + } + }) + }, + { + label: "User" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..577c84e3f --- /dev/null +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/members/$membershipId" +)({ + component: MemberDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/ssh/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Member + } + }) + }, + { + label: "User" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index af062aa13..95070c2c4 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -17,6 +17,7 @@ import { PageHeader } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -60,7 +61,7 @@ const Page = () => { }); handlePopUpClose("deleteRole"); navigate({ - to: "/projects/$projectId/access-management", + to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const, params: { projectId }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx index cedf07d05..abdf5c5d5 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx @@ -9,14 +9,16 @@ import { IconButton } from "@app/components/v2"; import { usePopUp } from "@app/hooks"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal"; type Props = { isDisabled?: boolean; + projectType: ProjectType; }; -export const AddPoliciesButton = ({ isDisabled }: Props) => { +export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => { const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addPolicy", "addPolicyOptions", @@ -66,10 +68,12 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => { handlePopUpToggle("addPolicy", isOpen)} /> handlePopUpToggle("applyTemplate", isOpen)} /> diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx index ccc3e4ea5..24974225e 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx @@ -7,6 +7,7 @@ import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { getProjectBaseURL } from "@app/helpers/project"; import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { TProjectRole } from "@app/hooks/api/roles/types"; import { slugSchema } from "@app/lib/schemas"; @@ -80,7 +81,7 @@ const Content = ({ role, onClose }: ContentProps) => { }); navigate({ - to: "/projects/$projectId/roles/$roleSlug", + to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug` as const, params: { roleSlug: newRole.slug, projectId: currentWorkspace.id diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx index a20bdf9b2..845c5f54e 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx @@ -19,26 +19,31 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { EXCLUDED_PERMISSION_SUBS, isConditionalSubjects, PROJECT_PERMISSION_OBJECT, + ProjectTypePermissionSubjects, TFormSchema } from "./ProjectRoleModifySection.utils"; type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; + type: ProjectType; }; type ContentProps = { onClose: () => void; + + type: ProjectType; }; type TForm = { permissions: Record }; -const Content = ({ onClose }: ContentProps) => { +const Content = ({ onClose, type: projectType }: ContentProps) => { const rootForm = useFormContext(); const [search, setSearch] = useState(""); const { @@ -56,7 +61,12 @@ const Content = ({ onClose }: ContentProps) => { }); const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT) - .filter(([, { title }]) => (search ? title.toLowerCase().includes(search.toLowerCase()) : true)) + .filter( + ([subject, { title }]) => + ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][ + subject as ProjectPermissionSub + ] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true) + ) .filter(([subject]) => !EXCLUDED_PERMISSION_SUBS.includes(subject as ProjectPermissionSub)) .sort((a, b) => a[1].title.localeCompare(b[1].title)) .map(([subject]) => subject); @@ -191,7 +201,7 @@ const Content = ({ onClose }: ContentProps) => { ); }; -export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => { +export const PolicySelectionModal = ({ isOpen, onOpenChange, type }: Props) => { return ( { subTitle="Select one or more policies to add to this role." className="max-w-3xl" > - onOpenChange(false)} /> + onOpenChange(false)} type={type} /> ); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx index 69c43d63a..d9cae6244 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx @@ -13,6 +13,7 @@ import { ModalContent } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { PROJECT_PERMISSION_OBJECT, @@ -24,19 +25,23 @@ import { type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; + type: ProjectType; }; type ContentProps = { onClose: () => void; + type: ProjectType; }; -const Content = ({ onClose }: ContentProps) => { +const Content = ({ onClose, type: projectType }: ContentProps) => { const rootForm = useFormContext(); const [selectedTemplate, setSelectedTemplate] = useState(); const [conflictingSubjects, setConflictingSubjects] = useState([]); const [showConflictingSubjects, setShowConflictingSubjects] = useState(false); + const templates = RoleTemplates[projectType ?? ProjectType.SecretManager]; + const onSubmit = (skipConflicting = false) => { if (!selectedTemplate) { createNotification({ type: "error", text: "Please select a template" }); @@ -121,12 +126,12 @@ const Content = ({ onClose }: ContentProps) => { type="single" value={selectedTemplate?.id} onValueChange={(value) => - setSelectedTemplate(RoleTemplates.find((template) => template.id === value)) + setSelectedTemplate(templates.find((template) => template.id === value)) } collapsible className="w-full border-collapse" > - {RoleTemplates.map(({ name, description, permissions, id }) => ( + {templates.map(({ name, description, permissions, id }) => ( { ); }; -export const PolicyTemplateModal = ({ isOpen, onOpenChange }: Props) => { +export const PolicyTemplateModal = ({ isOpen, onOpenChange, type }: Props) => { return ( { subTitle="Select a template with prepopulated policies to get started. You can always add more policies later." className="max-w-3xl" > - onOpenChange(false)} /> + onOpenChange(false)} type={type} /> ); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 4e051f1d1..471246d04 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -1645,41 +1645,378 @@ export type RoleTemplate = { permissions: { subject: ProjectPermissionSub; actions: string[] }[]; }; -export const RoleTemplates = [ - { - id: "project-manager", - name: "Project Management Policies", - description: "Grants access to manage project members and settings", - permissions: [ +const projectManagerTemplate = ( + additionalPermissions: RoleTemplate["permissions"] = [] +): RoleTemplate => ({ + id: "project-manager", + name: "Project Management Policies", + description: "Grants access to manage project members and settings", + permissions: [ + { + subject: ProjectPermissionSub.AuditLogs, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.Groups, + actions: Object.values(ProjectPermissionGroupActions) + }, + { + subject: ProjectPermissionSub.Member, + actions: Object.values(ProjectPermissionMemberActions) + }, + { + subject: ProjectPermissionSub.Identity, + actions: Object.values(ProjectPermissionIdentityActions) + }, + { + subject: ProjectPermissionSub.Project, + actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete] + }, + { subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) }, + { + subject: ProjectPermissionSub.Settings, + actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit] + }, + ...additionalPermissions + ] +}); + +export const RoleTemplates: Record = { + [ProjectType.SSH]: [ + { + id: "ssh-viewer", + name: "SSH Viewing Policies", + description: "Grants read access to SSH certificates and hosts", + permissions: [ + { + subject: ProjectPermissionSub.SshCertificateAuthorities, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SshCertificates, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SshCertificateTemplates, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SshHosts, + actions: [ProjectPermissionSshHostActions.Read] + }, + { + subject: ProjectPermissionSub.SshHostGroups, + actions: [ProjectPermissionActions.Read] + } + ] + }, + { + id: "ssh-cert-editor", + name: "SSH Certificate Editing Policies", + description: "Grants read and edit access to SSH certificates", + permissions: [ + { + subject: ProjectPermissionSub.SshCertificateAuthorities, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SshCertificates, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SshCertificateTemplates, + actions: Object.values(ProjectPermissionActions) + } + ] + }, + { + id: "ssh-host-editor", + name: "SSH Host Editing Policies", + description: "Grants read and edit access to SSH hosts", + permissions: [ + { + subject: ProjectPermissionSub.SshHosts, + actions: Object.values(ProjectPermissionSshHostActions) + }, + { + subject: ProjectPermissionSub.SshHostGroups, + actions: Object.values(ProjectPermissionActions) + } + ] + }, + projectManagerTemplate() + ], + [ProjectType.KMS]: [ + { + id: "kms-viewer", + name: "KMS Viewing Policies", + description: "Grants read access to KMS keys and KMIP clients", + permissions: [ + { + subject: ProjectPermissionSub.Cmek, + actions: [ProjectPermissionCmekActions.Read] + }, + { + subject: ProjectPermissionSub.Kmip, + actions: [ProjectPermissionKmipActions.ReadClients] + } + ] + }, + { + id: "key-editor", + name: "KMS Key Editing Policies", + description: "Grants read and edit access to KMS keys", + permissions: [ + { + subject: ProjectPermissionSub.Cmek, + actions: Object.values(ProjectPermissionCmekActions) + } + ] + }, + { + id: "kmip-editor", + name: "KMIP Client Editing Policies", + description: "Grants read and edit access to KMIP clients", + permissions: [ + { + subject: ProjectPermissionSub.Kmip, + actions: Object.values(ProjectPermissionKmipActions) + } + ] + }, + projectManagerTemplate() + ], + [ProjectType.CertificateManager]: [ + { + id: "cert-viewer", + name: "Certificate Viewing Policies", + description: "Grants read access to certificates and related resources", + permissions: [ + { + subject: ProjectPermissionSub.PkiCollections, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.PkiAlerts, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.CertificateAuthorities, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.CertificateTemplates, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.Certificates, + actions: [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.ReadPrivateKey + ] + } + ] + }, + { + id: "cert-editor", + name: "Certificate Editing Policies", + description: "Grants read and edit access to certificates and related resources", + permissions: [ + { + subject: ProjectPermissionSub.PkiCollections, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.PkiAlerts, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.CertificateAuthorities, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.CertificateTemplates, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.Certificates, + actions: Object.values(ProjectPermissionCertificateActions) + } + ] + }, + projectManagerTemplate() + ], + [ProjectType.SecretScanning]: [ + { + id: "scanning-viewer", + name: "Secret Scanning Viewing Policies", + description: "Grants read access to data sources and findings", + permissions: [ + { + subject: ProjectPermissionSub.SecretScanningDataSources, + actions: [ + ProjectPermissionSecretScanningDataSourceActions.Read, + ProjectPermissionSecretScanningDataSourceActions.ReadResources, + ProjectPermissionSecretScanningDataSourceActions.ReadScans + ] + }, + { + subject: ProjectPermissionSub.SecretScanningFindings, + actions: [ProjectPermissionSecretScanningFindingActions.Read] + }, + { + subject: ProjectPermissionSub.SecretScanningConfigs, + actions: [ProjectPermissionSecretScanningConfigActions.Read] + } + ] + }, + { + id: "scanning-editor", + name: "Secret Scanning Editing Policies", + description: "Grants read and edit access to data sources and findings", + permissions: [ + { + subject: ProjectPermissionSub.SecretScanningDataSources, + actions: Object.values(ProjectPermissionSecretScanningDataSourceActions) + }, + { + subject: ProjectPermissionSub.SecretScanningFindings, + actions: Object.values(ProjectPermissionSecretScanningFindingActions) + }, + { + subject: ProjectPermissionSub.SecretScanningConfigs, + actions: [ProjectPermissionSecretScanningConfigActions.Read] + } + ] + }, + projectManagerTemplate([ { - subject: ProjectPermissionSub.AuditLogs, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.Groups, - actions: Object.values(ProjectPermissionGroupActions) - }, - { - subject: ProjectPermissionSub.Member, - actions: Object.values(ProjectPermissionMemberActions) - }, - { - subject: ProjectPermissionSub.Identity, - actions: Object.values(ProjectPermissionIdentityActions) - }, - { - subject: ProjectPermissionSub.Project, - actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete] - }, - { subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) }, - { - subject: ProjectPermissionSub.Settings, - actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit] - }, + subject: ProjectPermissionSub.SecretScanningConfigs, + actions: Object.values(ProjectPermissionSecretScanningConfigActions) + } + ]) + ], + [ProjectType.SecretManager]: [ + { + id: "secret-viewer", + name: "Secret Viewing Policies", + description: "Grants read access to secrets and related resources", + permissions: [ + { + subject: ProjectPermissionSub.SecretRollback, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SecretImports, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.Secrets, + actions: [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue + ] + }, + { + subject: ProjectPermissionSub.DynamicSecrets, + actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential] + }, + { + subject: ProjectPermissionSub.Environments, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.Tags, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SecretRotation, + actions: [ProjectPermissionSecretRotationActions.Read] + }, + { + subject: ProjectPermissionSub.Integrations, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SecretSyncs, + actions: [ProjectPermissionSecretSyncActions.Read] + }, + { + subject: ProjectPermissionSub.Commits, + actions: [ProjectPermissionCommitsActions.Read] + } + ] + }, + { + id: "secret-editor", + name: "Secret Editing Policies", + description: "Grants read and edit access to secrets and related resources", + permissions: [ + { + subject: ProjectPermissionSub.Environments, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.DynamicSecrets, + actions: Object.values(ProjectPermissionDynamicSecretActions) + }, + { + subject: ProjectPermissionSub.Secrets, + actions: [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete + ] + }, + { + subject: ProjectPermissionSub.SecretRollback, + actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create] + }, + { + subject: ProjectPermissionSub.Tags, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SecretImports, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SecretRotation, + actions: Object.values(ProjectPermissionSecretRotationActions) + }, + { + subject: ProjectPermissionSub.SecretFolders, + actions: [ + ProjectPermissionActions.Create, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Delete + ] + }, + { + subject: ProjectPermissionSub.Integrations, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SecretSyncs, + actions: Object.values(ProjectPermissionSecretSyncActions) + }, + { + subject: ProjectPermissionSub.Commits, + actions: Object.values(ProjectPermissionCommitsActions) + } + ] + }, + projectManagerTemplate([ { subject: ProjectPermissionSub.IpAllowList, actions: Object.values(ProjectPermissionActions) }, + { + subject: ProjectPermissionSub.Kms, + actions: [ProjectPermissionActions.Edit] + }, { subject: ProjectPermissionSub.SecretApproval, actions: Object.values(ProjectPermissionActions) @@ -1692,314 +2029,6 @@ export const RoleTemplates = [ subject: ProjectPermissionSub.Webhooks, actions: Object.values(ProjectPermissionActions) } - ] - }, - { - id: "ssh-viewer", - name: "SSH Viewing Policies", - description: "Grants read access to SSH certificates and hosts", - permissions: [ - { - subject: ProjectPermissionSub.SshCertificateAuthorities, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.SshCertificates, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.SshCertificateTemplates, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.SshHosts, - actions: [ProjectPermissionSshHostActions.Read] - }, - { - subject: ProjectPermissionSub.SshHostGroups, - actions: [ProjectPermissionActions.Read] - } - ] - }, - { - id: "ssh-cert-editor", - name: "SSH Certificate Editing Policies", - description: "Grants read and edit access to SSH certificates", - permissions: [ - { - subject: ProjectPermissionSub.SshCertificateAuthorities, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.SshCertificates, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.SshCertificateTemplates, - actions: Object.values(ProjectPermissionActions) - } - ] - }, - { - id: "ssh-host-editor", - name: "SSH Host Editing Policies", - description: "Grants read and edit access to SSH hosts", - permissions: [ - { - subject: ProjectPermissionSub.SshHosts, - actions: Object.values(ProjectPermissionSshHostActions) - }, - { - subject: ProjectPermissionSub.SshHostGroups, - actions: Object.values(ProjectPermissionActions) - } - ] - }, - { - id: "kms-viewer", - name: "KMS Viewing Policies", - description: "Grants read access to KMS keys and KMIP clients", - permissions: [ - { - subject: ProjectPermissionSub.Cmek, - actions: [ProjectPermissionCmekActions.Read] - }, - { - subject: ProjectPermissionSub.Kmip, - actions: [ProjectPermissionKmipActions.ReadClients] - } - ] - }, - { - id: "key-editor", - name: "KMS Key Editing Policies", - description: "Grants read and edit access to KMS keys", - permissions: [ - { - subject: ProjectPermissionSub.Cmek, - actions: Object.values(ProjectPermissionCmekActions) - } - ] - }, - { - id: "kmip-editor", - name: "KMIP Client Editing Policies", - description: "Grants read and edit access to KMIP clients", - permissions: [ - { - subject: ProjectPermissionSub.Kmip, - actions: Object.values(ProjectPermissionKmipActions) - } - ] - }, - { - id: "cert-viewer", - name: "Certificate Viewing Policies", - description: "Grants read access to certificates and related resources", - permissions: [ - { - subject: ProjectPermissionSub.PkiCollections, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.PkiAlerts, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.CertificateAuthorities, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.CertificateTemplates, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.Certificates, - actions: [ - ProjectPermissionCertificateActions.Read, - ProjectPermissionCertificateActions.ReadPrivateKey - ] - } - ] - }, - { - id: "cert-editor", - name: "Certificate Editing Policies", - description: "Grants read and edit access to certificates and related resources", - permissions: [ - { - subject: ProjectPermissionSub.PkiCollections, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.PkiAlerts, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.CertificateAuthorities, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.CertificateTemplates, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.Certificates, - actions: Object.values(ProjectPermissionCertificateActions) - } - ] - }, - { - id: "scanning-viewer", - name: "Secret Scanning Viewing Policies", - description: "Grants read access to data sources and findings", - permissions: [ - { - subject: ProjectPermissionSub.SecretScanningDataSources, - actions: [ - ProjectPermissionSecretScanningDataSourceActions.Read, - ProjectPermissionSecretScanningDataSourceActions.ReadResources, - ProjectPermissionSecretScanningDataSourceActions.ReadScans - ] - }, - { - subject: ProjectPermissionSub.SecretScanningFindings, - actions: [ProjectPermissionSecretScanningFindingActions.Read] - }, - { - subject: ProjectPermissionSub.SecretScanningConfigs, - actions: [ProjectPermissionSecretScanningConfigActions.Read] - } - ] - }, - { - id: "scanning-editor", - name: "Secret Scanning Editing Policies", - description: "Grants read and edit access to data sources and findings", - permissions: [ - { - subject: ProjectPermissionSub.SecretScanningDataSources, - actions: Object.values(ProjectPermissionSecretScanningDataSourceActions) - }, - { - subject: ProjectPermissionSub.SecretScanningFindings, - actions: Object.values(ProjectPermissionSecretScanningFindingActions) - }, - { - subject: ProjectPermissionSub.SecretScanningConfigs, - actions: [ProjectPermissionSecretScanningConfigActions.Read] - } - ] - }, - { - id: "secret-viewer", - name: "Secret Viewing Policies", - description: "Grants read access to secrets and related resources", - permissions: [ - { - subject: ProjectPermissionSub.SecretRollback, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.SecretImports, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.Secrets, - actions: [ - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue - ] - }, - { - subject: ProjectPermissionSub.DynamicSecrets, - actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential] - }, - { - subject: ProjectPermissionSub.Environments, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.Tags, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.SecretRotation, - actions: [ProjectPermissionSecretRotationActions.Read] - }, - { - subject: ProjectPermissionSub.Integrations, - actions: [ProjectPermissionActions.Read] - }, - { - subject: ProjectPermissionSub.SecretSyncs, - actions: [ProjectPermissionSecretSyncActions.Read] - }, - { - subject: ProjectPermissionSub.Commits, - actions: [ProjectPermissionCommitsActions.Read] - } - ] - }, - { - id: "secret-editor", - name: "Secret Editing Policies", - description: "Grants read and edit access to secrets and related resources", - permissions: [ - { - subject: ProjectPermissionSub.Environments, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.DynamicSecrets, - actions: Object.values(ProjectPermissionDynamicSecretActions) - }, - { - subject: ProjectPermissionSub.Secrets, - actions: [ - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Delete - ] - }, - { - subject: ProjectPermissionSub.SecretRollback, - actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create] - }, - { - subject: ProjectPermissionSub.Tags, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.SecretImports, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.SecretRotation, - actions: Object.values(ProjectPermissionSecretRotationActions) - }, - { - subject: ProjectPermissionSub.SecretFolders, - actions: [ - ProjectPermissionActions.Create, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Delete - ] - }, - { - subject: ProjectPermissionSub.Integrations, - actions: Object.values(ProjectPermissionActions) - }, - { - subject: ProjectPermissionSub.SecretSyncs, - actions: Object.values(ProjectPermissionSecretSyncActions) - }, - { - subject: ProjectPermissionSub.Commits, - actions: Object.values(ProjectPermissionCommitsActions) - } - ] - } -]; + ]) + ] +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index 0f1433af3..eb7a2ba83 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -7,6 +7,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; import { useCreateProjectRole, useGetProjectRoleBySlug, @@ -100,7 +101,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }); navigate({ - to: "/projects/$projectId/roles/$roleSlug", + to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug` as const, params: { roleSlug: newRole.slug, projectId diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 216d514e4..dd43ad89c 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -14,6 +14,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { AddPoliciesButton } from "./AddPoliciesButton"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; @@ -121,6 +122,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { (role?.slug ?? "") as ProjectMembershipRole ); + const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager; + const permissions = form.watch("permissions"); const formattedPermissions = useMemo( @@ -171,7 +174,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { Save
    - +
    )} @@ -206,7 +209,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
    - {showAccessTree && ( + {isSecretManagerProject && showAccessTree && ( { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/cert-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Roles + } + }) + }, + { + label: "Roles" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx similarity index 79% rename from frontend/src/pages/project/RoleDetailsBySlugPage/route.tsx rename to frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx index c230df0a3..e1eae2756 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/route.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/roles/$roleSlug" + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug" )({ component: RoleDetailsBySlugPage, beforeLoad: ({ context, params }) => { @@ -15,7 +15,7 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/$projectId/access-management", + to: "/projects/kms/$projectId/access-management", params: { projectId: params.projectId }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx new file mode 100644 index 000000000..ad4d44d67 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/roles/$roleSlug" +)({ + component: RoleDetailsBySlugPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-management/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Roles + } + }) + }, + { + label: "Roles" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx new file mode 100644 index 000000000..6d5ad64e5 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug" +)({ + component: RoleDetailsBySlugPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/secret-scanning/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Roles + } + }) + }, + { + label: "Roles" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx new file mode 100644 index 000000000..1f06f795d --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/roles/$roleSlug" +)({ + component: RoleDetailsBySlugPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/projects/ssh/$projectId/access-management", + params: { + projectId: params.projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Roles + } + }) + }, + { + label: "Roles" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/SettingsPage/SettingsPage.tsx b/frontend/src/pages/project/SettingsPage/SettingsPage.tsx deleted file mode 100644 index 76d88947a..000000000 --- a/frontend/src/pages/project/SettingsPage/SettingsPage.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; - -import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; - -import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; - -const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }]; - -export const SettingsPage = () => { - const { t } = useTranslation(); - - return ( -
    - - {t("common.head-title", { title: t("settings.project.title") })} - -
    - - - - {tabs.map((tab) => ( - - {tab.name} - - ))} - - {tabs.map(({ key, Component }) => ( - - - - ))} - -
    -
    - ); -}; diff --git a/frontend/src/pages/project/SettingsPage/route.tsx b/frontend/src/pages/project/SettingsPage/route.tsx deleted file mode 100644 index 0718ea2f2..000000000 --- a/frontend/src/pages/project/SettingsPage/route.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -import { SettingsPage } from "./SettingsPage"; - -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/settings" -)({ - component: SettingsPage, - beforeLoad: ({ context }) => { - return { - breadcrumbs: [ - ...context.breadcrumbs, - { - label: "Settings" - } - ] - }; - } -}); diff --git a/frontend/src/pages/project/layout-general.tsx b/frontend/src/pages/project/layout-general.tsx deleted file mode 100644 index ae2f764d0..000000000 --- a/frontend/src/pages/project/layout-general.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -import { ProjectGeneralLayout } from "@app/layouts/ProjectGeneralLayout"; - -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout" -)({ - component: ProjectGeneralLayout -}); diff --git a/frontend/src/pages/project/layout.tsx b/frontend/src/pages/project/layout.tsx deleted file mode 100644 index 8f85cb1a5..000000000 --- a/frontend/src/pages/project/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -import { BreadcrumbTypes } from "@app/components/v2"; -import { workspaceKeys } from "@app/hooks/api"; -import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; -import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; -import { ProjectLayout } from "@app/layouts/ProjectLayout"; -import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; - -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout" -)({ - component: ProjectLayout, - beforeLoad: async ({ params, context }) => { - const project = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId) - }); - - await context.queryClient.ensureQueryData({ - queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId - }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) - }); - - return { - project, - breadcrumbs: [ - { - type: BreadcrumbTypes.Component, - component: ProjectSelect - } - ] - }; - } -}); diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx index 05a3c7231..06f79e9f0 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx @@ -34,7 +34,7 @@ export const CommitDetailsPage = () => { const handleGoBackToHistory = () => { navigate({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: currentWorkspace.id, folderId, @@ -49,7 +49,7 @@ export const CommitDetailsPage = () => { const handleGoToRollbackPreview = () => { navigate({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId/restore", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore", params: { projectId: currentWorkspace.id, folderId, diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx index c403f9f3f..483d00861 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from "react"; -import { faAngleDown } from "@fortawesome/free-solid-svg-icons"; +import { + faAngleDown, + faChevronLeft, + faCodeCommit, + faWarning +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu"; import { useSearch } from "@tanstack/react-router"; @@ -7,12 +12,14 @@ import { useSearch } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { + Button, + ContentLoader, DeleteActionModal, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, - IconButton, - Spinner + EmptyState, + PageHeader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { @@ -108,25 +115,25 @@ export const CommitDetailsTab = ({ // If no commit is selected or data is loading, show appropriate message if (!selectedCommitId) { return ( -
    -

    Select a commit to view details

    -
    + + + ); } if (isLoading) { - return ( -
    - -
    - ); + return ; } if (!commitDetails) { return ( -
    -

    No details found for this commit

    -
    + + + ); } @@ -138,9 +145,11 @@ export const CommitDetailsTab = ({ } catch (error) { console.error("Failed to parse commit details:", error); return ( -
    -

    Error parsing commit details

    -
    + + + ); } @@ -223,13 +232,12 @@ export const CommitDetailsTab = ({ // Render an item from the merged list const renderMergedItem = (item: MergedItem): JSX.Element => { return ( -
    - toggleItemCollapsed(id)} - /> -
    + toggleItemCollapsed(id)} + /> ); }; @@ -240,114 +248,104 @@ export const CommitDetailsTab = ({ "Unknown"; return ( -
    -
    -
    -
    -
    -
    -

    - {parsedCommitDetails.changes?.message || "No message"} -

    -
    -
    -
    -

    - Commited by - {actorDisplay} - on - - {formatDisplayDate( - parsedCommitDetails.changes?.createdAt || new Date().toISOString() - )} - - {parsedCommitDetails.changes?.isLatest && ( - (Latest) - )} -

    -
    -
    -
    - - {(isAllowed) => ( - - + + + Commited by {actorDisplay} on{" "} + {formatDisplayDate(parsedCommitDetails.changes?.createdAt || new Date().toISOString())} + {parsedCommitDetails.changes?.isLatest && ( + (Latest) + )} + + } + > + + {(isAllowed) => ( + + + + + + {!parsedCommitDetails.changes.isLatest && ( + goToRollbackPreview()} > - -

    Restore Options

    - -
    -
    - - {!parsedCommitDetails.changes.isLatest && ( - goToRollbackPreview()} - > -
    -
    - - Roll back to this commit - - - Return this folder to its exact state at the time of this commit, - discarding all other changes made after it - -
    -
    -
    - )} - - handlePopUpOpen("revertChanges")} - > -
    -
    - Revert changes - - Will restore to the previous version of affected resources - -
    +
    +
    + + Roll back to this commit + + + Return this folder to its exact state at the time of this commit, + discarding all other changes made after it +
    - - - - )} - -
    +
    +
    + )} + handlePopUpOpen("revertChanges")} + > +
    +
    + Revert changes + + Will restore to the previous version of affected resources + +
    +
    +
    +
    +
    + )} +
    + +
    +
    +

    Commit Changes

    - -
    -
    -
    - {sortedChangedItems.length > 0 ? ( - sortedChangedItems.map((item) => renderMergedItem(item)) - ) : ( -
    -

    No changed items found

    -
    - )} -
    +
    +
    + {sortedChangedItems.length > 0 ? ( + sortedChangedItems.map((item) => renderMergedItem(item)) + ) : ( + + )}
    - -
    + ); }; diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx index b14a5fe94..e06daea38 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx @@ -102,7 +102,7 @@ export const RollbackPreviewTab = (): JSX.Element => { const goBackToHistory = () => { navigate({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: currentWorkspace.id, folderId, diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route.tsx index d0d3aead0..f78b27b4e 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route.tsx @@ -12,7 +12,7 @@ const RollbackPreviewTabQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore" )({ component: RollbackPreviewTab, validateSearch: zodValidator(RollbackPreviewTabQueryParamsSchema), @@ -33,7 +33,7 @@ export const Route = createFileRoute( links: context.project.environments.map((el) => ({ label: el.name, link: linkOptions({ - to: "/projects/$projectId/secret-manager/secrets/$envSlug", + to: "/projects/secret-management/$projectId/secrets/$envSlug", params: { projectId: params.projectId, envSlug: el.slug @@ -56,7 +56,7 @@ export const Route = createFileRoute( { label: "Commits", link: linkOptions({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: params.projectId, environment: params.environment, @@ -70,7 +70,7 @@ export const Route = createFileRoute( { label: params.commitId, link: linkOptions({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId", params: { projectId: params.projectId, environment: params.environment, diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx index 7744d3626..789a17f0f 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx @@ -1,7 +1,10 @@ /* eslint-disable no-nested-ternary */ import { useCallback, useRef, useState } from "react"; -import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; +import { faChevronDown, faChevronUp, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { IconButton, Tooltip } from "@app/components/v2"; export interface Version { id?: string; @@ -32,6 +35,7 @@ interface SecretVersionDiffViewProps { showHeader?: boolean; customHeader?: JSX.Element; excludedFieldsHighlight?: string[]; + onDiscard?: VoidFunction; } const isObject = (obj: JsonValue): obj is JsonObject => { @@ -225,15 +229,12 @@ const renderJsonWithDiffs = ( const getLineClass = (different: boolean) => { if (!different) return "flex"; - return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"; + return isOldVersion + ? "flex bg-red-500/50 rounded-sm text-red-300" + : "flex bg-green-500/50 rounded-sm text-green-300"; }; - const getHighlightClass = (different: boolean) => { - if (!different) return ""; - return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1"; - }; - - const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " "; + const prefix = isDifferent ? (isOldVersion ? " -" : " +") : " "; const keyDisplay = keyName ? `"${keyName}": ` : ""; const comma = !isLastItem ? "," : ""; @@ -255,8 +256,8 @@ const renderJsonWithDiffs = (
    {prefix}
    {indent} - {keyName && {keyDisplay}} - {valueDisplay} + {keyName && {keyDisplay}} + {valueDisplay} {comma}
    @@ -269,8 +270,8 @@ const renderJsonWithDiffs = (
    {prefix}
    {indent} - {keyName && {keyDisplay}} - [] + {keyName && {keyDisplay}} + [] {comma}
    @@ -283,8 +284,8 @@ const renderJsonWithDiffs = (
    {prefix}
    {indent} - {keyName && {keyDisplay}} - {"{}"} + {keyName && {keyDisplay}} + {"{}"} {comma}
    @@ -320,16 +321,12 @@ const renderJsonWithDiffs = (
    - {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
    {indent} - {keyName && ( - - {keyDisplay} - - )} - [ + {keyName && {keyDisplay}} + [
    @@ -357,11 +354,11 @@ const renderJsonWithDiffs = (
    - {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
    {indent} - ] + ] {comma}
    @@ -376,16 +373,12 @@ const renderJsonWithDiffs = (
    - {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
    {indent} - {keyName && ( - - {keyDisplay} - - )} - {"{"} + {keyName && {keyDisplay}} + {"{"}
    @@ -414,12 +407,11 @@ const renderJsonWithDiffs = (
    - {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
    {indent} - {"}"} - {comma} + {"}"}
    @@ -475,7 +467,9 @@ const formatDeletedJson = (json: JsonValue): JSX.Element => { const cleanVersionForComparison = (version: Version): JsonValue => { const { id, version: versionNumber, ...cleanVersion } = version; - return cleanVersion; + return Object.fromEntries( + Object.entries(cleanVersion).filter((entry) => typeof entry[1] !== "undefined") + ); }; export const SecretVersionDiffView = ({ @@ -484,7 +478,8 @@ export const SecretVersionDiffView = ({ onToggleCollapse, showHeader = true, customHeader, - excludedFieldsHighlight = ["metadata", "tags"] + excludedFieldsHighlight = ["metadata", "tags"], + onDiscard }: SecretVersionDiffViewProps) => { const oldContainerRef = useRef(null); const newContainerRef = useRef(null); @@ -527,7 +522,7 @@ export const SecretVersionDiffView = ({ } oldVersionContent = ( -
    +
    {renderJsonWithDiffs( cleanOldVersion, diffPaths, @@ -543,7 +538,7 @@ export const SecretVersionDiffView = ({
    ); newVersionContent = ( -
    +
    {renderJsonWithDiffs( cleanNewVersion, diffPaths, @@ -583,19 +578,19 @@ export const SecretVersionDiffView = ({ if (item.isDeleted) { textStyle = "line-through text-red-300"; changeBadge = ( - + {isSecret ? "Secret" : "Folder"} Deleted ); } else if (item.isAdded) { changeBadge = ( - + {isSecret ? "Secret" : "Folder"} Added ); } else if (item.isUpdated) { changeBadge = ( - + {isSecret ? "Secret" : "Folder"} Updated ); @@ -615,32 +610,48 @@ export const SecretVersionDiffView = ({ tabIndex={0} aria-expanded={!collapsed} > -
    - {key} +
    +

    {key}

    {changeBadge}
    - + {onDiscard && ( + + + + + + )} +
    ); }; return ( -
    +
    {showHeader && renderHeader()} - {!collapsed && ( -
    -
    +
    +
    {oldVersionContent}
    - +
    {newVersionContent}
    diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx index 623b42050..0cd791c27 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx @@ -12,7 +12,7 @@ const CommitDetailsPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/" )({ component: CommitDetailsPage, validateSearch: zodValidator(CommitDetailsPageQueryParamsSchema), @@ -33,7 +33,7 @@ export const Route = createFileRoute( links: context.project.environments.map((el) => ({ label: el.name, link: linkOptions({ - to: "/projects/$projectId/secret-manager/secrets/$envSlug", + to: "/projects/secret-management/$projectId/secrets/$envSlug", params: { projectId: params.projectId, envSlug: el.slug @@ -56,7 +56,7 @@ export const Route = createFileRoute( { label: "Commits", link: linkOptions({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: params.projectId, environment: params.environment, diff --git a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx index 406a1b112..1acb76446 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx @@ -31,7 +31,7 @@ export const CommitsPage = () => { const handleSelectCommit = (commitId: string) => { navigate({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId", params: { projectId: currentWorkspace.id, folderId, @@ -52,7 +52,7 @@ export const CommitsPage = () => { title="Commits" description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states." /> - +

    Secret Snapshots have been officially renamed to Commits. Going forward, all secret changes will be tracked as Commits. If you made changes before this update, you can diff --git a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx index 8f529ba86..58191f389 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx @@ -1,29 +1,17 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { faArrowDownWideShort, faArrowUpWideShort, + faCodeCommit, faCopy, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { format, formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow } from "date-fns"; -import { Button, Input, Spinner } from "@app/components/v2"; +import { Button, ContentLoader, EmptyState, IconButton, Input } from "@app/components/v2"; import { CopyButton } from "@app/components/v2/CopyButton"; -import { useGetFolderCommitHistory } from "@app/hooks/api/folderCommits"; - -interface CommitActorMetadata { - email?: string; - name?: string; -} - -interface Commit { - id: string; - message: string; - createdAt: string; - actorType: string; - actorMetadata?: CommitActorMetadata; -} +import { Commit, useGetFolderCommitHistory } from "@app/hooks/api/folderCommits"; const formatTimeAgo = (timestamp: string): string => { return formatDistanceToNow(new Date(timestamp), { addSuffix: true }); @@ -40,58 +28,40 @@ const CommitItem = ({ onSelectCommit: (commitId: string, tab: string) => void; }) => { return ( -

    -
    -
    -
    -
    - -
    -

    - - {commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType} -

    committed

    - - -

    -
    -
    -
    - - -
    -
    +
    + ); }; @@ -108,24 +78,16 @@ const DateGroup = ({ onSelectCommit: (commitId: string, tab: string) => void; }) => { return ( -
    -
    -
    -
    -
    -
    -

    Commits on {date}

    +
    +
    + +

    Commits on {date}

    -
    -
    +
    {commits.map((commit) => ( -
    -
    - -
    -
    + ))}
    @@ -147,10 +109,8 @@ export const CommitHistoryTab = ({ const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); - const [offset, setOffset] = useState(0); - const [allCommits, setAllCommits] = useState([]); const debounceTimeoutRef = useRef(); - const limit = 5; + const limit = 10; // Debounce search term useEffect(() => { @@ -170,55 +130,20 @@ export const CommitHistoryTab = ({ }, [searchTerm]); const { - data: response, + data: groupedCommits, isLoading, - isFetching + fetchNextPage, + isFetchingNextPage, + hasNextPage } = useGetFolderCommitHistory({ workspaceId: projectId, environment, directory: secretPath, - offset, limit, search: debouncedSearchTerm, sort: sortDirection }); - const commits = response?.commits || []; - const hasMore = response?.hasMore || false; - - // Reset accumulated commits when search or sort changes - useEffect(() => { - setAllCommits([]); - setOffset(0); - }, [debouncedSearchTerm, sortDirection]); - - // Accumulate commits instead of replacing them - useEffect(() => { - if (commits.length > 0) { - if (offset === 0) { - // First load or after search/sort change - replace all commits - setAllCommits(commits); - } else { - // Subsequent loads - append new commits - setAllCommits((prev) => [...prev, ...commits]); - } - } - }, [commits, offset]); - - const groupedCommits = useMemo(() => { - return allCommits.reduce( - (acc, commit) => { - const date = format(new Date(commit.createdAt), "MMM d, yyyy"); - if (!acc[date]) { - acc[date] = []; - } - acc[date].push(commit); - return acc; - }, - {} as Record - ); - }, [allCommits]); - const handleSort = useCallback(() => { setSortDirection((prev) => (prev === "desc" ? "asc" : "desc")); }, []); @@ -227,50 +152,39 @@ export const CommitHistoryTab = ({ setSearchTerm(value); }, []); - const loadMoreCommits = useCallback(() => { - if (hasMore && !isFetching) { - setOffset((prev) => prev + limit); - } - }, [hasMore, isFetching, limit]); - return ( -
    +
    +

    Commit History

    } placeholder="Search commits..." - className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20" onChange={(e) => handleSearch(e.target.value)} value={searchTerm} aria-label="Search commits" /> -
    -
    - +
    - - {isLoading && offset === 0 ? ( -
    - -
    + {isLoading ? ( + ) : ( -
    - {Object.keys(groupedCommits).length > 0 ? ( +
    + {groupedCommits && Object.keys(groupedCommits).length > 0 ? ( <> {Object.entries(groupedCommits).map(([date, dateCommits]) => ( ) : ( -
    -
    + )} - - {hasMore && ( + {hasNextPage && (
    )} diff --git a/frontend/src/pages/secret-manager/CommitsPage/route.tsx b/frontend/src/pages/secret-manager/CommitsPage/route.tsx index e98fcc842..0e3cbb80a 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/route.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/route.tsx @@ -13,7 +13,7 @@ const CommitsPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/" )({ component: CommitsPage, validateSearch: zodValidator(CommitsPageQueryParamsSchema), @@ -34,7 +34,7 @@ export const Route = createFileRoute( links: context.project.environments.map((el) => ({ label: el.name, link: linkOptions({ - to: "/projects/$projectId/secret-manager/secrets/$envSlug", + to: "/projects/secret-management/$projectId/secrets/$envSlug", params: { projectId: params.projectId, envSlug: el.slug @@ -57,7 +57,7 @@ export const Route = createFileRoute( { label: "Commits", link: linkOptions({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: params.projectId, environment: params.environment, diff --git a/frontend/src/pages/secret-manager/IPAllowlistPage/route.tsx b/frontend/src/pages/secret-manager/IPAllowlistPage/route.tsx index 16ede0811..639289de2 100644 --- a/frontend/src/pages/secret-manager/IPAllowlistPage/route.tsx +++ b/frontend/src/pages/secret-manager/IPAllowlistPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { IPAllowListPage } from "./IPAllowlistPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/allowlist" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist" )({ component: () => IPAllowListPage }); diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx index 424835421..29627c5a7 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx @@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations"; import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/$integrationId" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId" )({ component: IntegrationDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,7 +15,7 @@ export const Route = createFileRoute( { label: "Integrations", link: linkOptions({ - to: "/projects/$projectId/secret-manager/integrations", + to: "/projects/secret-management/$projectId/integrations", params, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx index 4120a3da3..25c12adc2 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx @@ -42,7 +42,7 @@ export const redirectForProviderAuth = ( switch (integrationOption.slug) { case "gcp-secret-manager": navigate({ - to: "/projects/$projectId/secret-manager/integrations/gcp-secret-manager/authorize", + to: "/projects/secret-management/$projectId/integrations/gcp-secret-manager/authorize", params: { projectId } @@ -54,7 +54,7 @@ export const redirectForProviderAuth = ( return; } navigate({ - to: "/projects/$projectId/secret-manager/integrations/azure-key-vault/authorize", + to: "/projects/secret-management/$projectId/integrations/azure-key-vault/authorize", params: { projectId }, @@ -76,7 +76,7 @@ export const redirectForProviderAuth = ( } case "aws-parameter-store": navigate({ - to: "/projects/$projectId/secret-manager/integrations/aws-parameter-store/authorize", + to: "/projects/secret-management/$projectId/integrations/aws-parameter-store/authorize", params: { projectId } @@ -84,7 +84,7 @@ export const redirectForProviderAuth = ( break; case "aws-secret-manager": navigate({ - to: "/projects/$projectId/secret-manager/integrations/aws-secret-manager/authorize", + to: "/projects/secret-management/$projectId/integrations/aws-secret-manager/authorize", params: { projectId } @@ -120,7 +120,7 @@ export const redirectForProviderAuth = ( } case "github": navigate({ - to: "/projects/$projectId/secret-manager/integrations/github/auth-mode-selection", + to: "/projects/secret-management/$projectId/integrations/github/auth-mode-selection", params: { projectId } @@ -128,7 +128,7 @@ export const redirectForProviderAuth = ( break; case "gitlab": navigate({ - to: "/projects/$projectId/secret-manager/integrations/gitlab/authorize", + to: "/projects/secret-management/$projectId/integrations/gitlab/authorize", params: { projectId } @@ -136,7 +136,7 @@ export const redirectForProviderAuth = ( break; case "render": navigate({ - to: "/projects/$projectId/secret-manager/integrations/render/authorize", + to: "/projects/secret-management/$projectId/integrations/render/authorize", params: { projectId } @@ -144,7 +144,7 @@ export const redirectForProviderAuth = ( break; case "flyio": navigate({ - to: "/projects/$projectId/secret-manager/integrations/flyio/authorize", + to: "/projects/secret-management/$projectId/integrations/flyio/authorize", params: { projectId } @@ -152,7 +152,7 @@ export const redirectForProviderAuth = ( break; case "circleci": navigate({ - to: "/projects/$projectId/secret-manager/integrations/circleci/authorize", + to: "/projects/secret-management/$projectId/integrations/circleci/authorize", params: { projectId } @@ -160,7 +160,7 @@ export const redirectForProviderAuth = ( break; case "databricks": navigate({ - to: "/projects/$projectId/secret-manager/integrations/databricks/authorize", + to: "/projects/secret-management/$projectId/integrations/databricks/authorize", params: { projectId } @@ -168,7 +168,7 @@ export const redirectForProviderAuth = ( break; case "laravel-forge": navigate({ - to: "/projects/$projectId/secret-manager/integrations/laravel-forge/authorize", + to: "/projects/secret-management/$projectId/integrations/laravel-forge/authorize", params: { projectId } @@ -176,7 +176,7 @@ export const redirectForProviderAuth = ( break; case "travisci": navigate({ - to: "/projects/$projectId/secret-manager/integrations/travisci/authorize", + to: "/projects/secret-management/$projectId/integrations/travisci/authorize", params: { projectId } @@ -184,7 +184,7 @@ export const redirectForProviderAuth = ( break; case "supabase": navigate({ - to: "/projects/$projectId/secret-manager/integrations/supabase/authorize", + to: "/projects/secret-management/$projectId/integrations/supabase/authorize", params: { projectId } @@ -192,7 +192,7 @@ export const redirectForProviderAuth = ( break; case "checkly": navigate({ - to: "/projects/$projectId/secret-manager/integrations/checkly/authorize", + to: "/projects/secret-management/$projectId/integrations/checkly/authorize", params: { projectId } @@ -200,7 +200,7 @@ export const redirectForProviderAuth = ( break; case "qovery": navigate({ - to: "/projects/$projectId/secret-manager/integrations/qovery/authorize", + to: "/projects/secret-management/$projectId/integrations/qovery/authorize", params: { projectId } @@ -208,7 +208,7 @@ export const redirectForProviderAuth = ( break; case "railway": navigate({ - to: "/projects/$projectId/secret-manager/integrations/railway/authorize", + to: "/projects/secret-management/$projectId/integrations/railway/authorize", params: { projectId } @@ -216,7 +216,7 @@ export const redirectForProviderAuth = ( break; case "terraform-cloud": navigate({ - to: "/projects/$projectId/secret-manager/integrations/terraform-cloud/authorize", + to: "/projects/secret-management/$projectId/integrations/terraform-cloud/authorize", params: { projectId } @@ -224,7 +224,7 @@ export const redirectForProviderAuth = ( break; case "hashicorp-vault": navigate({ - to: "/projects/$projectId/secret-manager/integrations/hashicorp-vault/authorize", + to: "/projects/secret-management/$projectId/integrations/hashicorp-vault/authorize", params: { projectId } @@ -232,7 +232,7 @@ export const redirectForProviderAuth = ( break; case "cloudflare-pages": navigate({ - to: "/projects/$projectId/secret-manager/integrations/cloudflare-pages/authorize", + to: "/projects/secret-management/$projectId/integrations/cloudflare-pages/authorize", params: { projectId } @@ -240,7 +240,7 @@ export const redirectForProviderAuth = ( break; case "cloudflare-workers": navigate({ - to: "/projects/$projectId/secret-manager/integrations/cloudflare-workers/authorize", + to: "/projects/secret-management/$projectId/integrations/cloudflare-workers/authorize", params: { projectId } @@ -257,7 +257,7 @@ export const redirectForProviderAuth = ( } case "codefresh": navigate({ - to: "/projects/$projectId/secret-manager/integrations/codefresh/authorize", + to: "/projects/secret-management/$projectId/integrations/codefresh/authorize", params: { projectId } @@ -265,7 +265,7 @@ export const redirectForProviderAuth = ( break; case "digital-ocean-app-platform": navigate({ - to: "/projects/$projectId/secret-manager/integrations/digital-ocean-app-platform/authorize", + to: "/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/authorize", params: { projectId } @@ -273,7 +273,7 @@ export const redirectForProviderAuth = ( break; case "cloud-66": navigate({ - to: "/projects/$projectId/secret-manager/integrations/cloud-66/authorize", + to: "/projects/secret-management/$projectId/integrations/cloud-66/authorize", params: { projectId } @@ -281,7 +281,7 @@ export const redirectForProviderAuth = ( break; case "northflank": navigate({ - to: "/projects/$projectId/secret-manager/integrations/northflank/authorize", + to: "/projects/secret-management/$projectId/integrations/northflank/authorize", params: { projectId } @@ -289,7 +289,7 @@ export const redirectForProviderAuth = ( break; case "windmill": navigate({ - to: "/projects/$projectId/secret-manager/integrations/windmill/authorize", + to: "/projects/secret-management/$projectId/integrations/windmill/authorize", params: { projectId } @@ -297,7 +297,7 @@ export const redirectForProviderAuth = ( break; case "teamcity": navigate({ - to: "/projects/$projectId/secret-manager/integrations/teamcity/authorize", + to: "/projects/secret-management/$projectId/integrations/teamcity/authorize", params: { projectId } @@ -305,7 +305,7 @@ export const redirectForProviderAuth = ( break; case "hasura-cloud": navigate({ - to: "/projects/$projectId/secret-manager/integrations/hasura-cloud/authorize", + to: "/projects/secret-management/$projectId/integrations/hasura-cloud/authorize", params: { projectId } @@ -313,7 +313,7 @@ export const redirectForProviderAuth = ( break; case "rundeck": navigate({ - to: "/projects/$projectId/secret-manager/integrations/rundeck/authorize", + to: "/projects/secret-management/$projectId/integrations/rundeck/authorize", params: { projectId } @@ -321,7 +321,7 @@ export const redirectForProviderAuth = ( break; case "azure-devops": navigate({ - to: "/projects/$projectId/secret-manager/integrations/azure-devops/authorize", + to: "/projects/secret-management/$projectId/integrations/azure-devops/authorize", params: { projectId } @@ -329,7 +329,7 @@ export const redirectForProviderAuth = ( break; case "octopus-deploy": navigate({ - to: "/projects/$projectId/secret-manager/integrations/octopus-deploy/authorize", + to: "/projects/secret-management/$projectId/integrations/octopus-deploy/authorize", params: { projectId } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx index 194c543d2..df862b8ba 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx @@ -59,7 +59,7 @@ export const IntegrationRow = ({ navigate({ - to: "/projects/$projectId/secret-manager/integrations/$integrationId", + to: "/projects/secret-management/$projectId/integrations/$integrationId", params: { integrationId: integration.id, projectId: currentWorkspace.id diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/BitbucketSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/BitbucketSyncDestinationCol.tsx new file mode 100644 index 000000000..80243daec --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/BitbucketSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TBitbucketSync } from "@app/hooks/api/secretSyncs/types/bitbucket-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TBitbucketSync; +}; + +export const BitbucketSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 0a41b2c4b..10ffa9e8c 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -6,6 +6,7 @@ import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDest import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol"; import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; +import { BitbucketSyncDestinationCol } from "./BitbucketSyncDestinationCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; @@ -88,6 +89,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Supabase: return ; + case SecretSync.Bitbucket: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index a4a7e5480..e8d95344b 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -174,6 +174,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectName; secondaryText = "Supabase Project"; break; + case SecretSync.Bitbucket: + primaryText = destinationConfig.workspaceSlug; + secondaryText = destinationConfig.repositorySlug; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx index c59956459..b7f8236da 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx @@ -21,7 +21,7 @@ const IntegrationsListPageQuerySchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/" )({ component: IntegrationsListPage, validateSearch: zodValidator(IntegrationsListPageQuerySchema), @@ -36,7 +36,7 @@ export const Route = createFileRoute( }); } catch { throw redirect({ - to: "/projects/$projectId/secret-manager/integrations", + to: "/projects/secret-management/$projectId/integrations", params: { projectId }, @@ -46,7 +46,7 @@ export const Route = createFileRoute( if (secretSyncs.length) { throw redirect({ - to: "/projects/$projectId/secret-manager/integrations", + to: "/projects/secret-management/$projectId/integrations", params: { projectId }, @@ -62,7 +62,7 @@ export const Route = createFileRoute( }); } catch { throw redirect({ - to: "/projects/$projectId/secret-manager/integrations", + to: "/projects/secret-management/$projectId/integrations", params: { projectId }, @@ -72,7 +72,7 @@ export const Route = createFileRoute( if (integrations.length) { throw redirect({ - to: "/projects/$projectId/secret-manager/integrations", + to: "/projects/secret-management/$projectId/integrations", params: { projectId }, @@ -81,7 +81,7 @@ export const Route = createFileRoute( } throw redirect({ - to: "/projects/$projectId/secret-manager/integrations", + to: "/projects/secret-management/$projectId/integrations", params: { projectId }, diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index f58edbd33..61be72832 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -667,7 +667,7 @@ export const OverviewPage = () => { const envIndex = visibleEnvs.findIndex((el) => slug === el.slug); if (envIndex !== -1) { navigate({ - to: "/projects/$projectId/secret-manager/secrets/$envSlug", + to: "/projects/secret-management/$projectId/secrets/$envSlug", params: { projectId: workspaceId, envSlug: slug @@ -1420,7 +1420,7 @@ export const OverviewPage = () => { iconSize="3x" > { const navigate = useNavigate({ - from: "/projects/$projectId/secret-manager/overview" + from: "/projects/secret-management/$projectId/overview" }); const onFolderCrumbClick = (index: number) => { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx index 0d01370d4..79911b6ee 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx @@ -17,7 +17,7 @@ export const QuickSearchDynamicSecretItem = ({ onClose }: Props) => { const navigate = useNavigate({ - from: "/projects/$projectId/secret-manager/overview" + from: "/projects/secret-management/$projectId/overview" }); const [groupDynamicSecret] = dynamicSecretGroup; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx index 77f1f5215..92d2fac3e 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx @@ -13,7 +13,7 @@ type Props = { export const QuickSearchFolderItem = ({ folderGroup, onClose }: Props) => { const navigate = useNavigate({ - from: "/projects/$projectId/secret-manager/overview" + from: "/projects/secret-management/$projectId/overview" }); const [groupFolder] = folderGroup; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index c7a84f627..80b53d426 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -46,7 +46,7 @@ export const QuickSearchSecretItem = ({ isSingleEnv, search }: Props) => { - const navigate = useNavigate({ from: "/projects/$projectId/secret-manager/overview" }); + const navigate = useNavigate({ from: "/projects/secret-management/$projectId/overview" }); const envSlugMap = new Map(environments.map((env) => [env.slug, env])); const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ initialState: false diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx index 84869c656..fc4b66978 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx @@ -13,7 +13,7 @@ type Props = { export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }: Props) => { const navigate = useNavigate({ - from: "/projects/$projectId/secret-manager/overview" + from: "/projects/secret-management/$projectId/overview" }); const [groupSecretRotation] = secretRotationGroup; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/utils/index.ts b/frontend/src/pages/secret-manager/OverviewPage/components/utils/index.ts index 4441d842a..43caf7aa0 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/utils/index.ts +++ b/frontend/src/pages/secret-manager/OverviewPage/components/utils/index.ts @@ -1,6 +1,6 @@ export const getExpandedRowStyle = (scrollOffset: number) => ({ marginLeft: scrollOffset, - width: "calc(100vw - 355px)", // 350px accounts for sidebar and margin + width: "calc(100vw - 275px)", // accounts for sidebar and margin maxWidth: "1270px" // largest width of table on ultra-wide }); diff --git a/frontend/src/pages/secret-manager/OverviewPage/route.tsx b/frontend/src/pages/secret-manager/OverviewPage/route.tsx index 66b57d547..968286e13 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/route.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/route.tsx @@ -10,7 +10,7 @@ const SecretOverviewPageQuerySchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview" )({ component: OverviewPage, validateSearch: zodValidator(SecretOverviewPageQuerySchema), @@ -24,7 +24,7 @@ export const Route = createFileRoute( { label: "Secrets", link: linkOptions({ - to: "/projects/$projectId/secret-manager/overview", + to: "/projects/secret-management/$projectId/overview", params }) } diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index 430ec8492..7fd050b03 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -156,11 +156,11 @@ export const SecretApprovalRequestChangeItem = ({
    Tags
    -
    +
    {(secretVersion?.tags?.length ?? 0) ? ( secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
    - {Boolean(text.length) && "and"} + {Boolean(text.length) && " and "} {score[CommitType.DELETE]} Secret{score[CommitType.DELETE] !== 1 && "s"} Deleted diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx index abeb64951..dfc6d4e4d 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx @@ -9,7 +9,7 @@ const SecretApprovalPageQueryParams = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/approval" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval" )({ component: SecretApprovalsPage, validateSearch: zodValidator(SecretApprovalPageQueryParams), diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 1c95a202f..3342f6688 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-case-declarations */ import { useCallback, useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; @@ -48,6 +49,9 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits"; import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; +import { useCreateCommit } from "@app/hooks/api/secrets/mutations"; +import { SecretV3RawSanitized } from "@app/hooks/api/types"; import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal"; @@ -56,6 +60,7 @@ import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboar import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; import { ActionBar } from "./components/ActionBar"; +import { CommitForm } from "./components/CommitForm"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { DynamicSecretListView } from "./components/DynamicSecretListView"; import { FolderListView } from "./components/FolderListView"; @@ -65,8 +70,11 @@ import { SecretImportListView } from "./components/SecretImportListView"; import { SecretListView, SecretNoAccessListView } from "./components/SecretListView"; import { SnapshotView } from "./components/SnapshotView"; import { + PendingChanges, PopUpNames, StoreProvider, + useBatchMode, + useBatchModeActions, usePopUpAction, usePopUpState, useSelectedSecretActions, @@ -94,8 +102,11 @@ const Page = () => { }); const { permission } = useProjectPermission(); + const { mutateAsync: createCommit } = useCreateCommit(); const [isVisible, setIsVisible] = useState(false); + const { isBatchMode, pendingChanges } = useBatchMode(); + const { loadPendingChanges, setExistingKeys } = useBatchModeActions(); const { offset, @@ -128,6 +139,12 @@ const Page = () => { const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; + useEffect(() => { + if (isBatchMode && workspaceId && environment && secretPath) { + loadPendingChanges({ workspaceId, environment, secretPath }); + } + }, [isBatchMode, workspaceId, environment, secretPath, loadPendingChanges]); + const canReadSecret = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, @@ -217,7 +234,7 @@ const Page = () => { type: "error" }); navigate({ - to: "/projects/$projectId/secret-manager/overview", + to: "/projects/secret-management/$projectId/overview", params: { projectId: workspaceId } @@ -293,6 +310,30 @@ const Page = () => { }); const isProtectedBranch = Boolean(boardPolicy); + const handleCreateCommit = async (changes: PendingChanges, message: string) => { + try { + await createCommit({ + workspaceId, + environment, + secretPath, + pendingChanges: changes, + message + }); + createNotification({ + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Changes committed successfully", + type: "success" + }); + } catch (error) { + createNotification({ + text: "Failed to commit changes", + type: "error" + }); + console.error(error); + } + }; + const { data: snapshotList, isFetchingNextPage: isFetchingNextSnapshotList, @@ -345,7 +386,7 @@ const Page = () => { const handleOnClickRollbackMode = () => { if (isPITEnabled) { navigate({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: workspaceId, folderId, @@ -376,9 +417,21 @@ const Page = () => { imports?.length || dynamicSecrets?.length || secretRotations?.length || - noAccessSecretCount + noAccessSecretCount || + pendingChanges.secrets.length || + pendingChanges.folders.length ); + useEffect(() => { + if (data && isBatchMode) { + const existingSecretKeys = [...(secrets?.map((s) => s.key) || [])]; + + const existingFolderNames = folders?.map((f) => f.name) || []; + + setExistingKeys(existingSecretKeys, existingFolderNames); + } + }, [data, isBatchMode, setExistingKeys, secrets, importedSecrets, folders]); + const handleSortToggle = () => setOrderDirection((state) => state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC @@ -506,6 +559,143 @@ const Page = () => { setDebouncedSearchFilter(""); }; + const getMergedSecretsWithPending = () => { + if (!isBatchMode || pendingChanges.secrets.length === 0) { + return secrets; + } + + const mergedSecrets = [...(secrets || [])]; + + pendingChanges.secrets.forEach((change) => { + switch (change.type) { + case PendingAction.Create: + mergedSecrets.unshift({ + id: change.id, + key: change.secretKey, + value: change.secretValue, + comment: change.secretComment || "", + skipMultilineEncoding: change.skipMultilineEncoding || false, + tags: change.tags || [], + secretMetadata: change.secretMetadata || [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + version: 1, + isPending: true, + pendingAction: PendingAction.Create + } as unknown as SecretV3RawSanitized); + break; + + case PendingAction.Update: + const updateIndex = mergedSecrets.findIndex((s) => s.key === change.secretKey); + if (updateIndex >= 0) { + mergedSecrets[updateIndex] = { + ...mergedSecrets[updateIndex], + key: change.newSecretName || change.secretKey, + value: + change.secretValue !== undefined + ? change.secretValue + : mergedSecrets[updateIndex].value, + comment: + change.secretComment !== undefined + ? change.secretComment + : mergedSecrets[updateIndex].comment, + skipMultilineEncoding: + change.skipMultilineEncoding !== undefined + ? change.skipMultilineEncoding + : mergedSecrets[updateIndex].skipMultilineEncoding, + secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata, + isPending: true, + pendingAction: PendingAction.Update, + tags: change.tags + ? change.tags?.map((tag) => ({ + id: tag.id, + slug: tag.slug, + projectId: workspaceId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + __v: 0 + })) || [] + : mergedSecrets[updateIndex].tags + }; + } + break; + + case PendingAction.Delete: + const deleteIndex = mergedSecrets.findIndex((s) => s.key === change.secretKey); + if (deleteIndex >= 0) { + mergedSecrets[deleteIndex] = { + ...mergedSecrets[deleteIndex], + isPending: true, + pendingAction: PendingAction.Delete + }; + } + break; + + default: + break; + } + }); + + return mergedSecrets; + }; + + const getMergedFoldersWithPending = () => { + if (!isBatchMode || pendingChanges.folders.length === 0) { + return folders; + } + + const mergedFolders = [...(folders || [])]; + + pendingChanges.folders.forEach((change) => { + switch (change.type) { + case PendingAction.Create: + mergedFolders.unshift({ + id: change.id, + name: change.folderName, + description: change.description, + parentId: null, + isPending: true, + pendingAction: PendingAction.Create + } as any); + break; + + case PendingAction.Update: + const updateIndex = mergedFolders.findIndex((f) => f.id === change.id); + if (updateIndex >= 0) { + mergedFolders[updateIndex] = { + ...mergedFolders[updateIndex], + name: change.folderName, + description: + change.description !== undefined + ? change.description + : mergedFolders[updateIndex].description, + isPending: true, + pendingAction: PendingAction.Update + }; + } + break; + + case PendingAction.Delete: + const deleteIndex = mergedFolders.findIndex((f) => f.id === change.id); + if (deleteIndex >= 0) { + mergedFolders[deleteIndex] = { + ...mergedFolders[deleteIndex], + isPending: true, + pendingAction: PendingAction.Delete + }; + } + break; + + default: + break; + } + }); + + return mergedFolders; + }; + + const mergedSecrets = getMergedSecretsWithPending(); + const mergedFolders = getMergedFoldersWithPending(); return (
    { projectSlug={projectSlug} secretPath={secretPath} isVisible={isVisible} + isBatchMode={isBatchMode} filter={filter} tags={tags} onVisibilityToggle={handleToggleVisibility} @@ -698,9 +889,9 @@ const Page = () => { importedSecrets={importedSecrets} /> )} - {Boolean(folders?.length) && ( + {Boolean(mergedFolders?.length) && ( { {canReadSecretRotations && Boolean(secretRotations?.length) && ( )} - {canReadSecret && Boolean(secrets?.length) && ( + {canReadSecret && Boolean(mergedSecrets?.length) && ( { usedBySecretSyncs={usedBySecretSyncs} /> )} + {(pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0) && ( + + )} {noAccessSecretCount > 0 && } {!canReadSecret && !canReadDynamicSecret && @@ -738,25 +938,34 @@ const Page = () => { folders?.length === 0 && }
    - {!isDetailsLoading && totalCount > 0 && ( - - } - className="rounded-b-md border-t border-solid border-t-mineshaft-600" - count={totalCount} - page={page} - perPage={perPage} - onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={handlePerPageChange} - /> - )} + {!isDetailsLoading && + (totalCount > 0 || + pendingChanges.secrets.length > 0 || + pendingChanges.folders.length > 0) && ( + s.type === PendingAction.Create).length + } + folderCount={ + totalFolderCount + + pendingChanges.folders.filter((f) => f.type === PendingAction.Create).length + } + secretRotationCount={totalSecretRotationCount} + /> + } + className="rounded-b-md border-t border-solid border-t-mineshaft-600" + count={totalCount + pendingChanges.secrets.length + pendingChanges.folders.length} + page={page} + perPage={perPage} + onChangePage={(newPage) => setPage(newPage)} + onChangePerPage={handlePerPageChange} + /> + )} togglePopUp(PopUpNames.CreateSecretForm, state)} @@ -772,6 +981,7 @@ const Page = () => { secretPath={secretPath} autoCapitalize={currentWorkspace?.autoCapitalization} isProtectedBranch={isProtectedBranch} + isBatchMode={isBatchMode} /> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx index 0e4ecca95..15ee3b9c6 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx @@ -1,13 +1,218 @@ +/* eslint-disable no-nested-ternary */ import { createContext, ReactNode, useContext, useEffect, useRef } from "react"; import { useRouter } from "@tanstack/react-router"; import { createStore, StateCreator, StoreApi, useStore } from "zustand"; import { useShallow } from "zustand/react/shallow"; +import { createNotification } from "@app/components/notifications"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; // akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state // This is first step and the whole secret crud will be moved to this global page scope state // this will allow more stuff like undo grouping stuffs etc + +// Base interface for all pending changes +export interface BasePendingChange { + id: string; + timestamp: number; +} + +// Secret-related change types +export interface PendingSecretCreate extends BasePendingChange { + resourceType: "secret"; + type: PendingAction.Create; + secretKey: string; + secretValue: string; + secretComment?: string; + skipMultilineEncoding?: boolean; + tags?: { id: string; slug: string }[]; + secretMetadata?: { key: string; value: string }[]; + originalKey?: string; +} + +export interface PendingSecretUpdate extends BasePendingChange { + resourceType: "secret"; + type: PendingAction.Update; + secretKey: string; + newSecretName?: string; + originalValue?: string; + secretValue?: string; + originalComment?: string; + secretComment?: string; + originalSkipMultilineEncoding?: boolean; + skipMultilineEncoding?: boolean; + originalTags?: { id: string; slug: string }[]; + tags?: { id: string; slug: string }[]; + originalSecretMetadata?: { key: string; value: string }[]; + secretMetadata?: { key: string; value: string }[]; + existingSecret: SecretV3RawSanitized; +} + +export interface PendingSecretDelete extends BasePendingChange { + resourceType: "secret"; + type: PendingAction.Delete; + secretKey: string; + secretValue: string; +} + +// Folder-related change types +export interface PendingFolderCreate extends BasePendingChange { + resourceType: "folder"; + type: PendingAction.Create; + id: string; + folderName: string; + description?: string; + parentPath: string; +} + +export interface PendingFolderUpdate extends BasePendingChange { + resourceType: "folder"; + type: PendingAction.Update; + originalFolderName: string; + folderName: string; + id: string; + originalDescription?: string; + description?: string; +} + +export interface PendingFolderDelete extends BasePendingChange { + resourceType: "folder"; + type: PendingAction.Delete; + id: string; + folderName: string; + folderPath: string; +} + +// Union types for each resource +export type PendingSecretChange = PendingSecretCreate | PendingSecretUpdate | PendingSecretDelete; +export type PendingFolderChange = PendingFolderCreate | PendingFolderUpdate | PendingFolderDelete; +export type PendingChange = PendingSecretChange | PendingFolderChange; + +// Grouped changes for better processing +export interface PendingChanges { + secrets: PendingSecretChange[]; + folders: PendingFolderChange[]; +} + +// Context interface for batch operations +export interface BatchContext { + workspaceId: string; + environment: string; + secretPath: string; +} + +const normalizeValue = (value: any): string | boolean | undefined => { + if (value === null || value === undefined || value === "") { + return undefined; + } + return value; +}; + +const areValuesEqual = (value1: any, value2: any): boolean => { + const normalized1 = normalizeValue(value1); + const normalized2 = normalizeValue(value2); + + if (normalized1 === undefined && normalized2 === undefined) { + return true; + } + + return normalized1 === normalized2; +}; + +const areArraysEqual = (arr1: any[] | undefined, arr2: any[] | undefined): boolean => { + // Handle undefined/null arrays + if (!arr1 && !arr2) return true; + if (!arr1 || !arr2) return false; + + // Compare lengths + if (arr1.length !== arr2.length) return false; + + // Deep comparison using JSON stringify (for simple objects) + return JSON.stringify(arr1.sort()) === JSON.stringify(arr2.sort()); +}; + +const cleanupRevertedSecretFields = (update: PendingSecretUpdate): PendingSecretUpdate | null => { + const cleaned = { ...update }; + let hasChanges = false; + + if ( + cleaned.secretValue !== undefined && + !areValuesEqual(cleaned.secretValue, cleaned.originalValue) + ) { + hasChanges = true; + } else { + cleaned.secretValue = undefined; + } + + if ( + cleaned.secretComment !== undefined && + (!areValuesEqual(cleaned.secretComment, cleaned.originalComment) || + !areValuesEqual(cleaned.secretComment, cleaned.existingSecret.comment)) + ) { + hasChanges = true; + } else { + cleaned.secretComment = undefined; + } + + if ( + cleaned.skipMultilineEncoding !== undefined && + cleaned.skipMultilineEncoding !== cleaned.originalSkipMultilineEncoding + ) { + hasChanges = true; + } else { + cleaned.skipMultilineEncoding = undefined; + } + + if (cleaned.tags !== undefined && !areArraysEqual(cleaned.tags, cleaned.originalTags)) { + hasChanges = true; + } else { + cleaned.tags = undefined; + } + + if ( + cleaned.secretMetadata !== undefined && + !areArraysEqual(cleaned.secretMetadata, cleaned.originalSecretMetadata) + ) { + hasChanges = true; + } else { + cleaned.secretMetadata = undefined; + } + + if (cleaned.newSecretName !== undefined && cleaned.newSecretName !== cleaned.secretKey) { + hasChanges = true; + } else { + cleaned.newSecretName = undefined; + } + + // If no changes remain, return null to indicate this update should be removed + return hasChanges ? cleaned : null; +}; + +const cleanupRevertedFolderFields = (update: PendingFolderUpdate): PendingFolderUpdate | null => { + const cleaned = { ...update }; + let hasChanges = false; + + if ( + cleaned.folderName !== undefined && + !areValuesEqual(cleaned.folderName, cleaned.originalFolderName) + ) { + hasChanges = true; + } + + if ( + cleaned.description !== undefined && + !areValuesEqual(cleaned.description, cleaned.originalDescription) + ) { + hasChanges = true; + } else { + cleaned.description = undefined; + } + + // If no changes remain, return null to indicate this update should be removed + return hasChanges ? cleaned : null; +}; + type SelectedSecretState = { selectedSecret: Record; action: { @@ -16,14 +221,16 @@ type SelectedSecretState = { set: (secrets: Record) => void; }; }; -const createSelectedSecretStore: StateCreator = (set) => ({ + +const createSelectedSecretStore: StateCreator = ( + set +) => ({ selectedSecret: {}, action: { toggle: (secret) => set((state) => { const isChecked = Boolean(state.selectedSecret?.[secret.id]); const newChecks = { ...state.selectedSecret }; - // remove selection if its present else add it if (isChecked) delete newChecks[secret.id]; else newChecks[secret.id] = secret; return { selectedSecret: newChecks }; @@ -33,6 +240,432 @@ const createSelectedSecretStore: StateCreator = (set) => ({ } }); +type BatchModeState = { + isBatchMode: boolean; + pendingChanges: PendingChanges; + pendingChangesByContext: Map; + existingSecretKeys: Set; + existingFolderNames: Set; + currentContext: BatchContext | null; + batchActions: { + addPendingChange: (change: PendingChange, context: BatchContext) => void; + loadPendingChanges: (context: BatchContext) => void; + clearAllPendingChanges: (context: BatchContext) => void; + setExistingKeys: (secretKeys: string[], folderNames: string[]) => void; + getTotalPendingChangesCount: () => number; + removePendingChange: (changeId: string, resourceType: string, context: BatchContext) => void; + }; +}; + +const generateContextKey = (workspaceId: string, environment: string, secretPath: string) => { + return `${workspaceId}_${environment}_${secretPath}`; +}; + +const createBatchModeStore: StateCreator = (set, get) => ({ + isBatchMode: true, // Always enabled by default + pendingChanges: { secrets: [], folders: [] }, + pendingChangesByContext: new Map(), + currentContext: null, + existingSecretKeys: new Set(), + existingFolderNames: new Set(), + batchActions: { + addPendingChange: (change: PendingChange, context: BatchContext) => + set((state) => { + const contextKey = generateContextKey( + context.workspaceId, + context.environment, + context.secretPath + ); + + // Get existing changes for this context or create new empty state + const existingChanges = state.pendingChangesByContext.get(contextKey) || { + secrets: [], + folders: [] + }; + const newChanges = { ...existingChanges }; + + if (change.resourceType === "folder") { + const existingFolder = + state.existingFolderNames.has(change.folderName) || + newChanges.folders.some((f) => f.folderName === change.folderName); + + if (change.type === PendingAction.Create && existingFolder) { + createNotification({ + text: "A folder with this name already exists", + type: "error" + }); + return { pendingChanges: newChanges }; + } + if ( + change.type === PendingAction.Update && + change.folderName !== change.originalFolderName && + existingFolder + ) { + createNotification({ + text: "A folder with this name already exists", + type: "error" + }); + return { pendingChanges: newChanges }; + } + } + + if (change.resourceType === "secret") { + const existingSecret = + state.existingSecretKeys.has(change.secretKey) || + newChanges.secrets.some( + (s) => + (s.secretKey === change.secretKey && s.type !== PendingAction.Create) || + (change.type === PendingAction.Create && + change.originalKey !== change.secretKey && + s.secretKey === change.secretKey) + ); + + if (change.type === PendingAction.Create && existingSecret) { + createNotification({ + text: "A secret with this name already exists", + type: "error" + }); + return { pendingChanges: newChanges }; + } + + const existingNewSecretName = + change.type === PendingAction.Update && + change.newSecretName && + change.newSecretName !== change.secretKey && + (state.existingSecretKeys.has(change.newSecretName) || + newChanges.secrets.some( + (s) => + (s.secretKey === change.newSecretName || + (s.type === PendingAction.Update && + s.newSecretName === change.newSecretName)) && + s.id !== change.id + )); + + if (existingNewSecretName) { + createNotification({ + text: "A secret with this name already exists", + type: "error" + }); + return { pendingChanges: newChanges }; + } + } + + if (change.resourceType === "secret") { + const secretChanges = [...newChanges.secrets]; + + if (change.type === PendingAction.Create) { + const existingCreateIndex = secretChanges.findIndex( + (c) => + c.type === PendingAction.Create && + (c.secretKey === change.secretKey || c.secretKey === change.originalKey) + ); + + if (existingCreateIndex >= 0) { + secretChanges[existingCreateIndex] = { + ...secretChanges[existingCreateIndex], + ...change, + timestamp: Date.now() + }; + } else { + secretChanges.push(change); + } + } else if (change.type === PendingAction.Update) { + const existingCreateIndex = secretChanges.findIndex( + (c) => c.type === PendingAction.Create && c.id === change.id + ); + + if (existingCreateIndex >= 0) { + const existingCreate = secretChanges[existingCreateIndex] as PendingSecretCreate; + secretChanges[existingCreateIndex] = { + ...existingCreate, + secretKey: change.newSecretName || change.secretKey || existingCreate.secretKey, + secretValue: + change.secretValue !== undefined + ? change.secretValue + : existingCreate.secretValue, + secretComment: + change.secretComment !== undefined + ? change.secretComment + : existingCreate.secretComment, + skipMultilineEncoding: + change.skipMultilineEncoding !== undefined + ? change.skipMultilineEncoding + : existingCreate.skipMultilineEncoding, + tags: change.tags !== undefined ? change.tags : existingCreate.tags, + secretMetadata: + change.secretMetadata !== undefined + ? change.secretMetadata + : existingCreate.secretMetadata, + timestamp: Date.now() + }; + } else { + const existingUpdateIndex = secretChanges.findIndex( + (c) => c.type === PendingAction.Update && c.id === change.id + ); + + if (existingUpdateIndex >= 0) { + const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate; + const mergedUpdate: PendingSecretUpdate = { + ...existingUpdate, + secretKey: existingUpdate.secretKey, + originalValue: existingUpdate.originalValue, + originalComment: existingUpdate.originalComment, + originalSkipMultilineEncoding: existingUpdate.originalSkipMultilineEncoding, + originalTags: existingUpdate.originalTags, + originalSecretMetadata: existingUpdate.originalSecretMetadata, + + newSecretName: + change.newSecretName !== undefined + ? change.newSecretName + : existingUpdate.newSecretName, + secretValue: + change.secretValue !== undefined + ? change.secretValue + : existingUpdate.secretValue, + secretComment: + change.secretComment !== undefined + ? change.secretComment + : existingUpdate.secretComment, + skipMultilineEncoding: + change.skipMultilineEncoding !== undefined + ? change.skipMultilineEncoding + : existingUpdate.skipMultilineEncoding, + tags: change.tags !== undefined ? change.tags : existingUpdate.tags, + secretMetadata: + change.secretMetadata !== undefined + ? change.secretMetadata + : existingUpdate.secretMetadata, + existingSecret: existingUpdate.existingSecret, + timestamp: Date.now() + }; + + // Clean up reverted fields and check if any changes remain + const cleanedUpdate = cleanupRevertedSecretFields(mergedUpdate); + + if (cleanedUpdate) { + // Still has changes, keep the update + secretChanges[existingUpdateIndex] = cleanedUpdate; + } else { + // No changes remain, remove the pending update + secretChanges.splice(existingUpdateIndex, 1); + } + } else { + // New update - clean it up before adding + const cleanedUpdate = cleanupRevertedSecretFields(change); + if (cleanedUpdate) { + secretChanges.push(cleanedUpdate); + } + // If cleanedUpdate is null, don't add it (no actual changes) + } + } + } else { + secretChanges.push(change); + } + + newChanges.secrets = secretChanges; + } else if (change.resourceType === "folder") { + const folderChanges = [...newChanges.folders]; + + if (change.type === PendingAction.Create) { + const existingCreateIndex = folderChanges.findIndex( + (c) => c.type === PendingAction.Create && c.folderName === change.folderName + ); + + if (existingCreateIndex >= 0) { + folderChanges[existingCreateIndex] = { + ...folderChanges[existingCreateIndex], + ...change, + timestamp: Date.now() + }; + } else { + folderChanges.push(change); + } + } else if (change.type === PendingAction.Update) { + const existingCreateIndex = folderChanges.findIndex( + (c) => c.type === PendingAction.Create && c.folderName === change.originalFolderName + ); + + if (existingCreateIndex >= 0) { + const existingCreate = folderChanges[existingCreateIndex] as PendingFolderCreate; + folderChanges[existingCreateIndex] = { + ...existingCreate, + folderName: change.folderName || existingCreate.folderName, + description: + change.description !== undefined + ? change.description + : existingCreate.description, + timestamp: Date.now() + }; + } else { + const existingUpdateIndex = folderChanges.findIndex( + (c) => c.type === PendingAction.Update && c.id === change.id + ); + + if (existingUpdateIndex >= 0) { + const existingUpdate = folderChanges[existingUpdateIndex] as PendingFolderUpdate; + + const mergedUpdate: PendingFolderUpdate = { + ...existingUpdate, + originalFolderName: existingUpdate.originalFolderName, + originalDescription: existingUpdate.originalDescription, + + folderName: + change.folderName !== undefined ? change.folderName : existingUpdate.folderName, + description: + change.description !== undefined + ? change.description + : existingUpdate.description, + + timestamp: Date.now() + }; + + // Clean up reverted fields and check if any changes remain + const cleanedUpdate = cleanupRevertedFolderFields(mergedUpdate); + + if (cleanedUpdate) { + // Still has changes, keep the update + folderChanges[existingUpdateIndex] = cleanedUpdate; + } else { + // No changes remain, remove the pending update + folderChanges.splice(existingUpdateIndex, 1); + } + } else { + // New update - clean it up before adding + const cleanedUpdate = cleanupRevertedFolderFields(change); + if (cleanedUpdate) { + folderChanges.push(cleanedUpdate); + } + // If cleanedUpdate is null, don't add it (no actual changes) + } + } + } else { + folderChanges.push(change); + } + + newChanges.folders = folderChanges; + } + + const updatedContextMap = new Map(state.pendingChangesByContext); + updatedContextMap.set(contextKey, newChanges); + + const currentChanges = + contextKey === + generateContextKey( + state.currentContext?.workspaceId || context.workspaceId, + state.currentContext?.environment || context.environment, + state.currentContext?.secretPath || context.secretPath + ) + ? newChanges + : state.pendingChanges; + + return { + pendingChangesByContext: updatedContextMap, + pendingChanges: currentChanges, + currentContext: context + }; + }), + + removePendingChange: (changeId: string, resourceType: string, context: BatchContext) => + set((state) => { + const contextKey = generateContextKey( + context.workspaceId, + context.environment, + context.secretPath + ); + + // Get existing changes for this context + const existingChanges = state.pendingChangesByContext.get(contextKey) || { + secrets: [], + folders: [] + }; + const newChanges = { ...existingChanges }; + + if (resourceType === "secret") { + newChanges.secrets = newChanges.secrets.filter((c) => c.id !== changeId); + } else if (resourceType === "folder") { + newChanges.folders = newChanges.folders.filter((c) => c.id !== changeId); + } + + const updatedContextMap = new Map(state.pendingChangesByContext); + updatedContextMap.set(contextKey, newChanges); + + const isCurrentContext = + state.currentContext && + contextKey === + generateContextKey( + state.currentContext.workspaceId, + state.currentContext.environment, + state.currentContext.secretPath + ); + + return { + pendingChangesByContext: updatedContextMap, + pendingChanges: isCurrentContext ? newChanges : state.pendingChanges + }; + }), + + loadPendingChanges: (context) => { + const contextKey = generateContextKey( + context.workspaceId, + context.environment, + context.secretPath + ); + + set((state) => { + const contextChanges = state.pendingChangesByContext.get(contextKey) || { + secrets: [], + folders: [] + }; + + return { + pendingChanges: contextChanges, + currentContext: context + }; + }); + }, + + clearAllPendingChanges: (context) => { + const contextKey = generateContextKey( + context.workspaceId, + context.environment, + context.secretPath + ); + + set((state) => { + // Clear changes for this specific context + const updatedContextMap = new Map(state.pendingChangesByContext); + updatedContextMap.delete(contextKey); + + // If this is the current context, also clear the active pending changes + const isCurrentContext = + state.currentContext && + contextKey === + generateContextKey( + state.currentContext.workspaceId, + state.currentContext.environment, + state.currentContext.secretPath + ); + + return { + pendingChangesByContext: updatedContextMap, + pendingChanges: isCurrentContext ? { secrets: [], folders: [] } : state.pendingChanges + }; + }); + }, + + setExistingKeys: (secretKeys, folderNames) => + set({ + existingSecretKeys: new Set(secretKeys), + existingFolderNames: new Set(folderNames) + }), + + getTotalPendingChangesCount: () => { + const state = get(); + return state.pendingChanges.secrets.length + state.pendingChanges.folders.length; + } + } +}); + export enum PopUpNames { CreateSecretForm = "create-secret-form" } @@ -58,13 +691,14 @@ const createPopUpStore: StateCreator = (set) => ({ } }); -type CombinedState = SelectedSecretState & PopUpState; +type CombinedState = SelectedSecretState & PopUpState & BatchModeState; const StoreContext = createContext | null>(null); export const StoreProvider = ({ children }: { children: ReactNode }) => { const storeRef = useRef>( createStore((...a) => ({ ...createSelectedSecretStore(...a), - ...createPopUpStore(...a) + ...createPopUpStore(...a), + ...createBatchModeStore(...a) })) ); const router = useRouter(); @@ -99,3 +733,17 @@ export const useSelectedSecretActions = () => useStoreContext(useShallow((state) export const usePopUpState = (id: PopUpNames) => useStoreContext(useShallow((state) => state.popUp?.[id] || { isOpen: false })); export const usePopUpAction = () => useStoreContext(useShallow((state) => state.popUpActions)); + +export const useBatchMode = () => + useStoreContext( + useShallow((state) => ({ + isBatchMode: state.isBatchMode, + pendingChanges: state.pendingChanges, + currentContext: state.currentContext, + totalChangesCount: state.batchActions.getTotalPendingChangesCount(), + secretChangesCount: state.pendingChanges.secrets.length, + folderChangesCount: state.pendingChanges.folders.length + })) + ); + +export const useBatchModeActions = () => useStoreContext(useShallow((state) => state.batchActions)); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index e2680dbde..ea82cefb0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -77,12 +77,15 @@ import { } from "@app/hooks/api/dashboard/queries"; import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { + PendingFolderCreate, PopUpNames, + useBatchModeActions, usePopUpAction, useSelectedSecretActions, useSelectedSecrets @@ -112,6 +115,7 @@ type Props = { filter: Filter; tags?: WsTag[]; isVisible?: boolean; + isBatchMode?: boolean; snapshotCount: number; isSnapshotCountLoading?: boolean; protectedBranchPolicyName?: string; @@ -142,6 +146,7 @@ export const ActionBar = ({ filter, tags = [], isVisible, + isBatchMode, snapshotCount, isSnapshotCountLoading, onSearchChange, @@ -182,6 +187,7 @@ export const ActionBar = ({ options: { onSuccess: undefined } }); const queryClient = useQueryClient(); + const { addPendingChange } = useBatchModeActions(); const selectedSecrets = useSelectedSecrets(); const { reset: resetSelectedSecret } = useSelectedSecretActions(); @@ -192,6 +198,28 @@ export const ActionBar = ({ const handleFolderCreate = async (folderName: string, description: string | null) => { try { + if (isBatchMode) { + const folderId = `${folderName}`; + const pendingFolderCreate: PendingFolderCreate = { + id: folderId, + resourceType: "folder", + type: PendingAction.Create, + folderName, + description: description || undefined, + parentPath: secretPath, + timestamp: Date.now() + }; + + addPendingChange(pendingFolderCreate, { + workspaceId, + environment, + secretPath + }); + + handlePopUpClose("addFolder"); + return; + } + await createFolder({ name: folderName, path: secretPath, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx index 885838498..ab3a71af5 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx @@ -8,7 +8,12 @@ import { TextArea } from "@app/components/v2/TextArea/TextArea"; type Props = { onCreateFolder?: (folderName: string, description: string | null) => Promise; - onUpdateFolder?: (folderName: string, description: string | null) => Promise; + onUpdateFolder?: ( + folderName: string, + description: string | null, + oldFolderName?: string, + oldFolderDescription?: string + ) => Promise; isEdit?: boolean; defaultFolderName?: string; defaultDescription?: string; @@ -69,7 +74,7 @@ export const FolderForm = ({ const descriptionShaped = description && description.trim() !== "" ? description : null; if (isEdit) { - await onUpdateFolder?.(name, descriptionShaped); + await onUpdateFolder?.(name, descriptionShaped, defaultFolderName, defaultDescription); } else { await onCreateFolder?.(name, descriptionShaped); } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx new file mode 100644 index 000000000..c2e3ab12b --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -0,0 +1,446 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import React, { useCallback, useState } from "react"; +import { faCodeCommit, faEye, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; +import { SecretVersionDiffView } from "@app/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView"; + +import { + PendingChange, + PendingChanges, + useBatchMode, + useBatchModeActions +} from "../../SecretMainPage.store"; + +interface CommitFormProps { + onCommit: (changes: PendingChanges, commitMessage: string) => Promise; + isCommitting?: boolean; + environment: string; + workspaceId: string; + secretPath: string; +} + +interface ResourceChangeProps { + change: PendingChange; + environment: string; + workspaceId: string; + secretPath: string; +} + +type RenderResourceProps = { + onDiscard: () => void; + change: PendingChange; +}; + +const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => { + if (change.resourceType !== "secret") return null; + + if (change.type === PendingAction.Create) { + return ( + + ); + } + + if (change.type === PendingAction.Update) { + const { existingSecret } = change; + + const hasKeyChange = change.newSecretName && change.secretKey !== change.newSecretName; + const hasValueChange = change.secretValue !== change.originalValue; + const hasCommentChange = change.secretComment !== change.originalComment; + const hasMultilineChange = + change.skipMultilineEncoding !== change.originalSkipMultilineEncoding; + const hasTagsChange = JSON.stringify(change.tags) !== JSON.stringify(change.originalTags); + const hasMetadataChange = + JSON.stringify(change.secretMetadata) !== JSON.stringify(change.originalSecretMetadata); + + const hasChanges = [ + hasKeyChange, + hasValueChange, + hasCommentChange, + hasMultilineChange, + hasTagsChange, + hasMetadataChange + ].some(Boolean); + + if (!hasChanges) return null; + + return ( + tag.slug) ?? []) : undefined, + secretMetadata: change.secretMetadata ? existingSecret.secretMetadata : undefined, + skipMultilineEncoding: + typeof change.skipMultilineEncoding === "boolean" + ? existingSecret.skipMultilineEncoding + : undefined, + comment: change.secretComment !== undefined ? existingSecret.comment : undefined + }, + { + version: 2, // placeholder, not used + secretKey: change.newSecretName, + secretValue: change.secretValue, + tags: change.tags?.map((tag) => tag.slug), + secretMetadata: change.secretMetadata, + skipMultilineEncoding: change.skipMultilineEncoding, + comment: change.secretComment + } + ] + }} + /> + ); + } + + if (change.type === PendingAction.Delete) { + const { secretKey, secretValue } = change; + return ( + + ); + } + + return null; +}; + +const RenderFolderChanges = ({ onDiscard, change }: RenderResourceProps) => { + if (change.resourceType !== "folder") return null; + + if (change.type === PendingAction.Create) { + return ( + + ); + } + + if (change.type === PendingAction.Update) { + const hasNameChange = change.folderName !== change.originalFolderName; + const hasDescriptionChange = change.description !== change.originalDescription; + + const hasChanges = [hasNameChange, hasDescriptionChange].some(Boolean); + + if (!hasChanges) return null; + + return ( + + ); + } + + if (change.type === PendingAction.Delete) { + return ( + + ); + } + + return null; +}; + +const ResourceChange: React.FC = ({ + change, + environment, + workspaceId, + secretPath +}) => { + const { removePendingChange } = useBatchModeActions(); + + const handleDeletePending = useCallback( + (changeType: string, id: string) => { + removePendingChange(id, changeType, { + workspaceId, + environment, + secretPath + }); + }, + [change.resourceType, change.id] + ); + + return change.resourceType === "secret" ? ( + handleDeletePending(change.resourceType, change.id)} + /> + ) : ( + handleDeletePending(change.resourceType, change.id)} + /> + ); +}; + +export const CommitForm: React.FC = ({ + onCommit, + isCommitting = false, + environment, + workspaceId, + secretPath +}) => { + const { isBatchMode, pendingChanges, totalChangesCount } = useBatchMode(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [commitMessage, setCommitMessage] = useState(""); + const { clearAllPendingChanges } = useBatchModeActions(); + + if (!isBatchMode || totalChangesCount === 0) { + return null; + } + + const handleCommit = async () => { + if (!commitMessage.trim()) { + return; + } + await onCommit(pendingChanges, commitMessage); + clearAllPendingChanges({ + workspaceId, + environment, + secretPath + }); + setIsModalOpen(false); + setCommitMessage(""); + }; + + return ( + <> + {/* Floating Panel */} + {!isModalOpen && ( +
    +
    + {/* Left Content */} +
    + {/* Header */} +
    +
    + Pending Changes + + {totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""} + +
    + + {/* Description */} +

    + Review pending changes and commit them to apply the updates. +

    +
    + + {/* Right Buttons */} +
    + + +
    +
    +
    + )} + + {/* Commit Modal */} + + + + Commit Changes + + {totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""} + +
    + } + subTitle={"Write a commit message and review the changes you're about to commit."} + className="max-h-[90vh] max-w-[95%] md:max-w-7xl" + > +
    + {/* Changes List */} +
    +
    + {/* Folder Changes */} + {pendingChanges.folders.length > 0 && ( +
    +

    + + Folders ({pendingChanges.folders.length}) +

    +
    + {pendingChanges.folders.map((change) => ( + + ))} +
    +
    + )} + + {/* Secret Changes */} + {pendingChanges.secrets.length > 0 && ( +
    +

    + + Secrets ({pendingChanges.secrets.length}) +

    +
    + {pendingChanges.secrets.map((change) => ( + + ))} +
    +
    + )} +
    +
    + + {/* Commit Message */} +
    + + setCommitMessage(e.target.value)} + placeholder="Describe your changes..." + className="w-full" + required + /> +
    + + {/* Action Buttons */} +
    + + +
    +
    + + + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/index.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/index.tsx new file mode 100644 index 000000000..47502d9a5 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/index.tsx @@ -0,0 +1 @@ +export { CommitForm } from "./CommitForm"; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx index a4318d53a..9b43d820d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -12,9 +12,15 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { getKeyValue } from "@app/helpers/parseEnvVar"; import { useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { SecretType } from "@app/hooks/api/types"; -import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store"; +import { + PendingSecretCreate, + PopUpNames, + useBatchModeActions, + usePopUpAction +} from "../../SecretMainPage.store"; const typeSchema = z.object({ key: z.string().trim().min(1, { message: "Secret key is required" }), @@ -31,6 +37,7 @@ type Props = { // modal props autoCapitalize?: boolean; isProtectedBranch?: boolean; + isBatchMode?: boolean; }; export const CreateSecretForm = ({ @@ -38,7 +45,8 @@ export const CreateSecretForm = ({ workspaceId, secretPath = "/", autoCapitalize = true, - isProtectedBranch = false + isProtectedBranch = false, + isBatchMode = false }: Props) => { const { register, @@ -53,6 +61,7 @@ export const CreateSecretForm = ({ const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const createWsTag = useCreateWsTag(); + const { addPendingChange } = useBatchModeActions(); const { permission } = useProjectPermission(); const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); @@ -85,6 +94,26 @@ export const CreateSecretForm = ({ const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => { try { + if (isBatchMode) { + const pendingSecretCreate: PendingSecretCreate = { + id: key, + type: PendingAction.Create, + secretKey: key, + secretValue: value || "", + secretComment: "", + tags: tags?.map((el) => ({ id: el.value, slug: el.label })), + timestamp: Date.now(), + resourceType: "secret" + }; + addPendingChange(pendingSecretCreate, { + workspaceId, + environment, + secretPath + }); + closePopUp(PopUpNames.CreateSecretForm); + reset(); + return; + } await createSecretV3({ environment, workspaceId, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx index d652d49ad..57ede8d0c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx @@ -1,10 +1,5 @@ import { subject } from "@casl/ability"; -import { - faClose, - faFingerprint, - faPencilSquare, - faWarning -} from "@fortawesome/free-solid-svg-icons"; +import { faEdit, faFingerprint, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; @@ -187,7 +182,7 @@ export const DynamicSecretListView = ({ )}
    -
    +
    - + )} @@ -228,6 +223,7 @@ export const DynamicSecretListView = ({ { @@ -236,7 +232,7 @@ export const DynamicSecretListView = ({ }} isDisabled={!isAllowed || isRevoking} > - + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx index b95e73fb8..e0cc3f78b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx @@ -1,7 +1,15 @@ import { subject } from "@casl/ability"; -import { faClose, faFolder, faInfoCircle, faPencilSquare } from "@fortawesome/free-solid-svg-icons"; +import { + faClose, + faEdit, + faFolder, + faInfoCircle, + faPencilSquare, + faTrash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; @@ -11,8 +19,15 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api"; -import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; +import { PendingAction, TSecretFolder } from "@app/hooks/api/secretFolders/types"; +import { + PendingFolderCreate, + PendingFolderDelete, + PendingFolderUpdate, + useBatchMode, + useBatchModeActions +} from "../../SecretMainPage.store"; import { FolderForm } from "../ActionBar/FolderForm"; type Props = { @@ -42,10 +57,62 @@ export const FolderListView = ({ const { mutateAsync: updateFolder } = useUpdateFolder(); const { mutateAsync: deleteFolder } = useDeleteFolder(); + const { isBatchMode } = useBatchMode(); + const { addPendingChange, removePendingChange } = useBatchModeActions(); - const handleFolderUpdate = async (newFolderName: string, newFolderDescription: string | null) => { + const handleFolderUpdate = async ( + newFolderName: string, + newFolderDescription: string | null, + oldFolderName?: string, + oldFolderDescription?: string + ) => { try { - const { id: folderId } = popUp.updateFolder.data as TSecretFolder; + const updateFolderData = popUp.updateFolder.data; + if (!updateFolderData) throw new Error("Update folder data is required"); + const { id: folderId, pendingAction, isPending } = updateFolderData as TSecretFolder; + + if (isBatchMode) { + const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create; + + if (isEditingPendingCreation) { + const updatedCreate: PendingFolderCreate = { + id: folderId, + type: PendingAction.Create, + folderName: newFolderName, + description: newFolderDescription || undefined, + parentPath: secretPath, + timestamp: Date.now(), + resourceType: "folder" + }; + + addPendingChange(updatedCreate, { + workspaceId, + environment, + secretPath + }); + } else { + const updateChange: PendingFolderUpdate = { + id: folderId, + type: PendingAction.Update, + originalFolderName: oldFolderName || "", + folderName: newFolderName, + originalDescription: oldFolderDescription, + description: newFolderDescription || undefined, + timestamp: Date.now(), + resourceType: "folder" + }; + + addPendingChange(updateChange, { + workspaceId, + environment, + secretPath + }); + } + + handlePopUpClose("updateFolder"); + return; + } + await updateFolder({ folderId, name: newFolderName, @@ -68,15 +135,45 @@ export const FolderListView = ({ } }; + const handleDeletePending = (id: string) => { + removePendingChange(id, "folder", { + workspaceId, + environment, + secretPath + }); + }; + const handleFolderDelete = async () => { try { - const { id: folderId } = popUp.deleteFolder.data as TSecretFolder; + const folderData = popUp.deleteFolder?.data as TSecretFolder; + + if (isBatchMode) { + const pendingFolderDelete: PendingFolderDelete = { + id: folderData.id, + folderName: folderData.name, + folderPath: secretPath, + resourceType: "folder", + type: PendingAction.Delete, + timestamp: Date.now() + }; + + addPendingChange(pendingFolderDelete, { + workspaceId, + environment, + secretPath + }); + + handlePopUpClose("deleteFolder"); + return; + } + await deleteFolder({ - folderId, + folderId: folderData.id, path: secretPath, environment, projectId: workspaceId }); + handlePopUpClose("deleteFolder"); createNotification({ type: "success", @@ -91,7 +188,10 @@ export const FolderListView = ({ } }; - const handleFolderClick = (name: string) => { + const handleFolderClick = (name: string, isPending?: boolean) => { + if (isPending) { + return; + } const path = `${secretPathQueryparam === "/" ? "" : secretPathQueryparam}/${name}`; navigate({ search: (el) => ({ ...el, secretPath: path }) @@ -100,10 +200,16 @@ export const FolderListView = ({ return ( <> - {folders.map(({ name, id, description }) => ( + {folders.map(({ name, id, description, pendingAction, isPending }) => (
    @@ -113,9 +219,9 @@ export const FolderListView = ({ role="button" tabIndex={0} onKeyDown={(evt) => { - if (evt.key === "Enter") handleFolderClick(name); + if (evt.key === "Enter") handleFolderClick(name, isPending); }} - onClick={() => handleFolderClick(name)} + onClick={() => handleFolderClick(name, isPending)} > {name} {description && ( @@ -128,46 +234,72 @@ export const FolderListView = ({ )}
    -
    - - {(isAllowed) => ( - handlePopUpOpen("updateFolder", { id, name, description })} - isDisabled={!isAllowed} - > - - - )} - - - {(isAllowed) => ( - handlePopUpOpen("deleteFolder", { id, name })} - isDisabled={!isAllowed} - > - - - )} - -
    + {isPending ? ( +
    + {}} + > + + + + handleDeletePending(id)} + > + + +
    + ) : ( +
    + + {(isAllowed) => ( + handlePopUpOpen("updateFolder", { id, name, description })} + isDisabled={!isAllowed} + > + + + )} + + + {(isAllowed) => ( + handlePopUpOpen("deleteFolder", { id, name })} + isDisabled={!isAllowed} + > + + + )} + +
    + )}
    ))} )}
    -
    +
    - + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 61dbdb4be..fbd4565aa 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -52,6 +52,7 @@ import { useWorkspace } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { getProjectBaseURL } from "@app/helpers/project"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetSecretVersion } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; @@ -980,7 +981,9 @@ export const SecretDetailSidebar = ({ className="z-[100] capitalize" > - Save Changes + Apply Changes )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index bee8266f6..707284fcf 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -38,7 +38,7 @@ import { WsTag } from "@app/hooks/api/types"; import { subject } from "@casl/ability"; import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; -import { memo, useEffect } from "react"; +import { memo, useCallback, useEffect, useRef } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { twMerge } from "tailwind-merge"; import { @@ -50,6 +50,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { FontAwesomeSpriteName, formSchema, @@ -57,6 +58,7 @@ import { TFormSchema } from "./SecretListView.utils"; import { CollapsibleSecretImports } from "./CollapsibleSecretImports"; +import { useBatchModeActions } from "../../SecretMainPage.store"; export const HIDDEN_SECRET_VALUE = "******"; export const HIDDEN_SECRET_VALUE_API_MASK = ""; @@ -86,6 +88,8 @@ type Props = { isImported: boolean; }[]; }[]; + isPending?: boolean; + pendingAction?: PendingAction; }; export const SecretItem = memo( @@ -102,7 +106,9 @@ export const SecretItem = memo( environment, secretPath, handleSecretShare, - importedBy + importedBy, + isPending, + pendingAction }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "editSecret" @@ -110,6 +116,18 @@ export const SecretItem = memo( const { currentWorkspace } = useWorkspace(); const { permission } = useProjectPermission(); const { isRotatedSecret } = secret; + const { removePendingChange } = useBatchModeActions(); + + const autoSaveTimeoutRef = useRef(); + const isAutoSavingRef = useRef(false); + + const handleDeletePending = (pendingSecret: SecretV3RawSanitized) => { + removePendingChange(pendingSecret.id, "secret", { + workspaceId: currentWorkspace.id, + environment, + secretPath + }); + }; const canEditSecretValue = permission.can( ProjectPermissionSecretActions.Edit, @@ -151,7 +169,6 @@ export const SecretItem = memo( }); const secretName = watch("key"); - const overrideAction = watch("overrideAction"); const hasComment = Boolean(watch("comment")); @@ -167,6 +184,56 @@ export const SecretItem = memo( name: "tags" }); + const isOverriden = + overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; + const hasTagsApplied = Boolean(fields.length); + + const autoSaveChanges = useCallback( + async (data: TFormSchema) => { + if (isAutoSavingRef.current) return; + if ( + data.overrideAction === SecretActionType.Created || + data.overrideAction === SecretActionType.Modified + ) { + return; + } + + isAutoSavingRef.current = true; + try { + await onSaveSecret(secret, { ...secret, ...data }, () => { + reset(); + }); + } catch (error) { + console.error("Auto-save failed:", error); + } finally { + isAutoSavingRef.current = false; + } + }, + [secret, onSaveSecret, importedBy, reset] + ); + + const formValues = watch(); + + useEffect(() => { + if (autoSaveTimeoutRef.current) { + clearTimeout(autoSaveTimeoutRef.current); + } + + if (isDirty && !isSubmitting && !isAutoSavingRef.current) { + const debounceTime = 600; + + autoSaveTimeoutRef.current = setTimeout(() => { + autoSaveChanges(formValues); + }, debounceTime); + } + + return () => { + if (autoSaveTimeoutRef.current) { + clearTimeout(autoSaveTimeoutRef.current); + } + }; + }, [formValues, isDirty, isSubmitting, autoSaveChanges, isPending]); + const isReadOnly = hasSecretReadValueOrDescribePermission( permission, @@ -188,6 +255,9 @@ export const SecretItem = memo( }) ); + const isReadOnlySecret = + isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete); + const { secretValueHidden } = secret; const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); @@ -199,10 +269,6 @@ export const SecretItem = memo( return () => clearTimeout(timer); }, [isSecValueCopied]); - const isOverriden = - overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; - const hasTagsApplied = Boolean(fields.length); - const handleOverrideClick = () => { if (isOverriden) { // override need not be flagged delete if it was never saved in server @@ -267,20 +333,27 @@ export const SecretItem = memo( setIsSecValueCopied.on(); }; + const isInAutoSaveMode = isDirty && !isSubmitting && !isOverriden; + return (
    {secret.isRotatedSecret ? ( @@ -364,7 +437,7 @@ export const SecretItem = memo( control={control} render={({ field }) => ( )} -
    - - - - - - - {(isAllowed) => ( - - - - - - - - - e.preventDefault()} // prevents secret input from displaying value on open - > - - - - )} - - - - {(isAllowed) => ( - - - - - - - - )} - - - Add tags to this secret - {tags.map((tag) => { - const { id: tagId, slug, color } = tag; - - const isTagSelected = selectedTagsGroupById?.[tagId]; - return ( - handleTagSelect(tag)} - key={`${secret.id}-${tagId}`} - icon={ - isTagSelected && ( - - ) - } - iconPos="right" - > -
    -
    - {slug} -
    - - ); - })} - - - - - - - {(isAllowed) => ( + - - )} - - - - {(isAllowed) => ( - - - - - - - - )} - - - - - - - - -