diff --git a/backend/e2e-test/mocks/queue.ts b/backend/e2e-test/mocks/queue.ts index 58eebdedf..04a78bcd1 100644 --- a/backend/e2e-test/mocks/queue.ts +++ b/backend/e2e-test/mocks/queue.ts @@ -24,6 +24,7 @@ export const mockQueue = (): TQueueServiceFactory => { events[name] = event; }, getRepeatableJobs: async () => [], + getDelayedJobs: async () => [], clearQueue: async () => {}, stopJobById: async () => {}, stopJobByIdPg: async () => {}, diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 2956be192..1d2461acf 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -93,6 +93,7 @@ import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service"; +import { TReminderServiceFactory } from "@app/services/reminder/reminder-types"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; @@ -285,6 +286,7 @@ declare module "fastify" { secretScanningV2: TSecretScanningV2ServiceFactory; internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory; pkiTemplate: TPkiTemplatesServiceFactory; + reminder: TReminderServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 7ead9f84b..7fac20c0e 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -504,6 +504,12 @@ import { TProjectMicrosoftTeamsConfigsInsert, TProjectMicrosoftTeamsConfigsUpdate } from "@app/db/schemas/project-microsoft-teams-configs"; +import { TReminders, TRemindersInsert, TRemindersUpdate } from "@app/db/schemas/reminders"; +import { + TRemindersRecipients, + TRemindersRecipientsInsert, + TRemindersRecipientsUpdate +} from "@app/db/schemas/reminders-recipients"; import { TSecretReminderRecipients, TSecretReminderRecipientsInsert, @@ -1211,5 +1217,11 @@ declare module "knex/types/tables" { TSecretScanningConfigsInsert, TSecretScanningConfigsUpdate >; + [TableName.Reminder]: KnexOriginal.CompositeTableType; + [TableName.ReminderRecipient]: KnexOriginal.CompositeTableType< + TRemindersRecipients, + TRemindersRecipientsInsert, + TRemindersRecipientsUpdate + >; } } diff --git a/backend/src/db/migrations/20250701202824_add-reminder-table.ts b/backend/src/db/migrations/20250701202824_add-reminder-table.ts new file mode 100644 index 000000000..f1f5bc84d --- /dev/null +++ b/backend/src/db/migrations/20250701202824_add-reminder-table.ts @@ -0,0 +1,43 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.Reminder))) { + await knex.schema.createTable(TableName.Reminder, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("secretId").nullable(); + t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE"); + t.string("message", 1024).nullable(); + t.integer("repeatDays").checkPositive().nullable(); + t.timestamp("nextReminderDate").notNullable(); + t.timestamps(true, true, true); + t.unique("secretId"); + }); + } + + if (!(await knex.schema.hasTable(TableName.ReminderRecipient))) { + await knex.schema.createTable(TableName.ReminderRecipient, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("reminderId").notNullable(); + t.foreign("reminderId").references("id").inTable(TableName.Reminder).onDelete("CASCADE"); + t.uuid("userId").notNullable(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.timestamps(true, true, true); + t.index("reminderId"); + t.index("userId"); + t.unique(["reminderId", "userId"]); + }); + } + + await createOnUpdateTrigger(knex, TableName.Reminder); + await createOnUpdateTrigger(knex, TableName.ReminderRecipient); +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.Reminder); + await dropOnUpdateTrigger(knex, TableName.ReminderRecipient); + await knex.schema.dropTableIfExists(TableName.ReminderRecipient); + await knex.schema.dropTableIfExists(TableName.Reminder); +} 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..d18c38e34 --- /dev/null +++ b/backend/src/db/migrations/20250718133527_project-unify-revert.ts @@ -0,0 +1,432 @@ +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/models.ts b/backend/src/db/schemas/models.ts index 75d36833b..2e71486bf 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -160,7 +160,7 @@ export enum TableName { SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings", MicrosoftTeamsIntegrations = "microsoft_teams_integrations", ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs", - SecretReminderRecipients = "secret_reminder_recipients", + SecretReminderRecipients = "secret_reminder_recipients", // TODO(Carlos): Remove this in the future after migrating to the new reminder recipients table GithubOrgSyncConfig = "github_org_sync_configs", FolderCommit = "folder_commits", FolderCommitChanges = "folder_commit_changes", @@ -172,7 +172,10 @@ export enum TableName { SecretScanningResource = "secret_scanning_resources", SecretScanningScan = "secret_scanning_scans", SecretScanningFinding = "secret_scanning_findings", - SecretScanningConfig = "secret_scanning_configs" + SecretScanningConfig = "secret_scanning_configs", + // reminders + Reminder = "reminders", + ReminderRecipient = "reminders_recipients" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; @@ -267,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/db/schemas/reminders-recipients.ts b/backend/src/db/schemas/reminders-recipients.ts new file mode 100644 index 000000000..8e02dfc35 --- /dev/null +++ b/backend/src/db/schemas/reminders-recipients.ts @@ -0,0 +1,20 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const RemindersRecipientsSchema = z.object({ + id: z.string().uuid(), + reminderId: z.string().uuid(), + userId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TRemindersRecipients = z.infer; +export type TRemindersRecipientsInsert = Omit, TImmutableDBKeys>; +export type TRemindersRecipientsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/reminders.ts b/backend/src/db/schemas/reminders.ts new file mode 100644 index 000000000..6656ad077 --- /dev/null +++ b/backend/src/db/schemas/reminders.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const RemindersSchema = z.object({ + id: z.string().uuid(), + secretId: z.string().uuid().nullable().optional(), + message: z.string().nullable().optional(), + repeatDays: z.number().nullable().optional(), + nextReminderDate: z.date(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TReminders = z.infer; +export type TRemindersInsert = Omit, TImmutableDBKeys>; +export type TRemindersUpdate = Partial, TImmutableDBKeys>>; 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-endpoints.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts index 17fe14dbf..e6d7a50fe 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts @@ -315,10 +315,12 @@ export const registerSecretRotationEndpoints = < querystring: z.object({ deleteSecrets: z .enum(["true", "false"]) + .optional() .transform((value) => value === "true") .describe(SecretRotations.DELETE(type).deleteSecrets), revokeGeneratedCredentials: z .enum(["true", "false"]) + .optional() .transform((value) => value === "true") .describe(SecretRotations.DELETE(type).revokeGeneratedCredentials) }), 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/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 8f3f84e00..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", @@ -467,7 +468,11 @@ export enum EventType { CREATE_PROJECT = "create-project", UPDATE_PROJECT = "update-project", - DELETE_PROJECT = "delete-project" + DELETE_PROJECT = "delete-project", + + CREATE_SECRET_REMINDER = "create-secret-reminder", + GET_SECRET_REMINDER = "get-secret-reminder", + DELETE_SECRET_REMINDER = "delete-secret-reminder" } export const filterableSecretEvents: EventType[] = [ @@ -1546,8 +1551,9 @@ interface UpdateFolderEvent { metadata: { environment: string; folderId: string; - oldFolderName: string; + oldFolderName?: string; newFolderName: string; + newFolderDescription?: string; folderPath: string; }; } @@ -3222,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: { @@ -3312,6 +3330,31 @@ interface SecretScanningConfigUpdateEvent { }; } +interface SecretReminderCreateEvent { + type: EventType.CREATE_SECRET_REMINDER; + metadata: { + secretId: string; + message?: string | null; + repeatDays?: number | null; + nextReminderDate?: string | null; + recipients?: string[] | null; + }; +} + +interface SecretReminderGetEvent { + type: EventType.GET_SECRET_REMINDER; + metadata: { + secretId: string; + }; +} + +interface SecretReminderDeleteEvent { + type: EventType.DELETE_SECRET_REMINDER; + metadata: { + secretId: string; + }; +} + interface SecretScanningConfigReadEvent { type: EventType.SECRET_SCANNING_CONFIG_GET; metadata?: Record; // not needed, based off projectId @@ -3658,6 +3701,7 @@ export type Event = | PitRevertCommitEvent | PitCompareFolderStatesEvent | PitGetFolderStateEvent + | PitProcessNewCommitRawEvent | SecretScanningDataSourceListEvent | SecretScanningDataSourceGetEvent | SecretScanningDataSourceCreateEvent @@ -3674,4 +3718,7 @@ export type Event = | OrgUpdateEvent | ProjectCreateEvent | ProjectUpdateEvent - | ProjectDeleteEvent; + | ProjectDeleteEvent + | SecretReminderCreateEvent + | SecretReminderGetEvent + | SecretReminderDeleteEvent; 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/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 762be864d..5c8ad80bf 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -566,6 +566,14 @@ export const gatewayServiceFactory = ({ if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId); + + const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways." + }); + } + const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: orgGatewayConfig.orgId 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 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 b90d0ed88..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 @@ -46,6 +46,13 @@ import { TMySqlCredentialsRotationListItem, TMySqlCredentialsRotationWithConnection } from "./mysql-credentials"; +import { + TOktaClientSecretRotation, + TOktaClientSecretRotationGeneratedCredentials, + TOktaClientSecretRotationInput, + TOktaClientSecretRotationListItem, + TOktaClientSecretRotationWithConnection +} from "./okta-client-secret"; import { TOracleDBCredentialsRotation, TOracleDBCredentialsRotationInput, @@ -69,7 +76,8 @@ export type TSecretRotationV2 = | TAuth0ClientSecretRotation | TAzureClientSecretRotation | TLdapPasswordRotation - | TAwsIamUserSecretRotation; + | TAwsIamUserSecretRotation + | TOktaClientSecretRotation; export type TSecretRotationV2WithConnection = | TPostgresCredentialsRotationWithConnection @@ -79,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 @@ -96,7 +106,8 @@ export type TSecretRotationV2Input = | TAuth0ClientSecretRotationInput | TAzureClientSecretRotationInput | TLdapPasswordRotationInput - | TAwsIamUserSecretRotationInput; + | TAwsIamUserSecretRotationInput + | TOktaClientSecretRotationInput; export type TSecretRotationV2ListItem = | TPostgresCredentialsRotationListItem @@ -106,7 +117,8 @@ export type TSecretRotationV2ListItem = | TAuth0ClientSecretRotationListItem | TAzureClientSecretRotationListItem | TLdapPasswordRotationListItem - | TAwsIamUserSecretRotationListItem; + | TAwsIamUserSecretRotationListItem + | TOktaClientSecretRotationListItem; export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined; 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/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..7e670af96 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 @@ -592,7 +596,7 @@ export const secretScanningV2QueueServiceFactory = async ({ numberOfSecrets: payload.numberOfSecrets, isDiffScan: payload.isDiffScan, url: encodeURI( - `${appCfg.SITE_URL}/projects/${projectId}/secret-scanning/findings?search=scanId:${payload.scanId}` + `${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}` ), timestamp } @@ -603,7 +607,7 @@ export const secretScanningV2QueueServiceFactory = async ({ timestamp, errorMessage: payload.errorMessage, url: encodeURI( - `${appCfg.SITE_URL}/projects/${projectId}/secret-scanning/data-sources/${dataSource.type}/${dataSource.id}` + `${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}` ) } }); 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..b6c00985a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2289,6 +2289,13 @@ export const AppConnections = { SUPABASE: { accessKey: "The Key used to access Supabase.", instanceUrl: "The URL used to access Supabase." + }, + DIGITAL_OCEAN_APP_PLATFORM: { + apiToken: "The API token used to authenticate with Digital Ocean App Platform." + }, + OKTA: { + instanceUrl: "The URL used to access your Okta organization.", + apiToken: "The API token used to authenticate with Okta." } } }; @@ -2502,6 +2509,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 +2606,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 +2631,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/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 9f4d1c67f..bcef34d4f 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -64,7 +64,9 @@ export enum QueueName { FolderTreeCheckpoint = "folder-tree-checkpoint", InvalidateCache = "invalidate-cache", SecretScanningV2 = "secret-scanning-v2", - TelemetryAggregatedEvents = "telemetry-aggregated-events" + TelemetryAggregatedEvents = "telemetry-aggregated-events", + DailyReminders = "daily-reminders", + SecretReminderMigration = "secret-reminder-migration" } export enum QueueJobs { @@ -104,7 +106,9 @@ export enum QueueJobs { SecretScanningV2SendNotification = "secret-scanning-v2-notification", CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber", PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal", - TelemetryAggregatedEvents = "telemetry-aggregated-events" + TelemetryAggregatedEvents = "telemetry-aggregated-events", + DailyReminders = "daily-reminders", + SecretReminderMigration = "secret-reminder-migration" } export type TQueueJobTypes = { @@ -291,6 +295,14 @@ export type TQueueJobTypes = { caType: CaType; }; }; + [QueueName.DailyReminders]: { + name: QueueJobs.DailyReminders; + payload: undefined; + }; + [QueueName.SecretReminderMigration]: { + name: QueueJobs.SecretReminderMigration; + payload: undefined; + }; [QueueName.PkiSubscriber]: { name: QueueJobs.PkiSubscriberDailyAutoRenewal; payload: undefined; @@ -390,6 +402,11 @@ export type TQueueServiceFactory = { startOffset?: number, endOffset?: number ) => Promise<{ key: string; name: string; id: string | null }[]>; + getDelayedJobs: ( + name: QueueName, + startOffset?: number, + endOffset?: number + ) => Promise<{ delay: number; timestamp: number; repeatJobKey?: string; data?: unknown }[]>; }; export const queueServiceFactory = ( @@ -552,6 +569,13 @@ export const queueServiceFactory = ( return q.getRepeatableJobs(startOffset, endOffset); }; + const getDelayedJobs: TQueueServiceFactory["getDelayedJobs"] = (name, startOffset, endOffset) => { + const q = queueContainer[name]; + if (!q) throw new Error(`Queue '${name}' not initialized`); + + return q.getDelayed(startOffset, endOffset); + }; + const stopRepeatableJobByJobId: TQueueServiceFactory["stopRepeatableJobByJobId"] = async (name, jobId) => { const q = queueContainer[name]; const job = await q.getJob(jobId); @@ -598,6 +622,7 @@ export const queueServiceFactory = ( stopJobById, stopJobByIdPg, getRepeatableJobs, + getDelayedJobs, startPg, queuePg, schedulePg diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4f7ebeed9..ea4cf9676 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -246,6 +246,10 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; +import { reminderDALFactory } from "@app/services/reminder/reminder-dal"; +import { dailyReminderQueueServiceFactory } from "@app/services/reminder/reminder-queue"; +import { reminderServiceFactory } from "@app/services/reminder/reminder-service"; +import { reminderRecipientDALFactory } from "@app/services/reminder-recipients/reminder-recipient-dal"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { secretDALFactory } from "@app/services/secret/secret-dal"; @@ -371,6 +375,9 @@ export const registerRoutes = async ( const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db); const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db); + const reminderDAL = reminderDALFactory(db); + const reminderRecipientDAL = reminderRecipientDALFactory(db); + const integrationDAL = integrationDALFactory(db); const integrationAuthDAL = integrationAuthDALFactory(db); const webhookDAL = webhookDALFactory(db); @@ -734,9 +741,17 @@ export const registerRoutes = async ( const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); + const reminderService = reminderServiceFactory({ + reminderDAL, + reminderRecipientDAL, + smtpService, + projectMembershipDAL, + permissionService, + secretV2BridgeDAL + }); + const orgService = orgServiceFactory({ userAliasDAL, - queueService, identityMetadataDAL, secretDAL, secretV2BridgeDAL, @@ -762,7 +777,8 @@ export const registerRoutes = async ( orgBotDAL, oidcConfigDAL, loginService, - projectBotService + projectBotService, + reminderService }); const signupService = authSignupServiceFactory({ tokenService, @@ -1060,7 +1076,6 @@ export const registerRoutes = async ( secretImportDAL, projectEnvDAL, webhookDAL, - orgDAL, auditLogService, userDAL, projectMembershipDAL, @@ -1082,11 +1097,11 @@ export const registerRoutes = async ( secretApprovalRequestDAL, projectKeyDAL, projectUserMembershipRoleDAL, - secretReminderRecipientsDAL, orgService, resourceMetadataDAL, folderCommitService, - secretSyncQueue + secretSyncQueue, + reminderService }); const projectService = projectServiceFactory({ @@ -1095,7 +1110,6 @@ export const registerRoutes = async ( projectSshConfigDAL, secretDAL, secretV2BridgeDAL, - queueService, projectQueue: projectQueueService, projectBotService, identityProjectDAL, @@ -1132,7 +1146,8 @@ export const registerRoutes = async ( microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, - smtpService + smtpService, + reminderService }); const projectEnvService = projectEnvServiceFactory({ @@ -1231,6 +1246,7 @@ export const registerRoutes = async ( kmsService, snapshotService, resourceMetadataDAL, + reminderService, keyStore }); @@ -1284,7 +1300,8 @@ export const registerRoutes = async ( secretApprovalRequestSecretDAL, secretV2BridgeService, secretApprovalRequestService, - licenseService + licenseService, + reminderService }); const secretSharingService = secretSharingServiceFactory({ @@ -1538,7 +1555,12 @@ export const registerRoutes = async ( folderService, permissionService, folderDAL, - projectEnvDAL + projectEnvDAL, + secretApprovalRequestService, + secretApprovalPolicyService, + projectDAL, + secretV2BridgeService, + folderCommitDAL }); const identityOidcAuthService = identityOidcAuthServiceFactory({ @@ -1611,7 +1633,6 @@ export const registerRoutes = async ( auditLogDAL, queueService, secretVersionDAL, - secretDAL, secretFolderVersionDAL: folderVersionDAL, snapshotDAL, identityAccessTokenDAL, @@ -1622,6 +1643,13 @@ export const registerRoutes = async ( orgService }); + const dailyReminderQueueService = dailyReminderQueueServiceFactory({ + reminderService, + queueService, + secretDAL: secretV2BridgeDAL, + secretReminderRecipientsDAL + }); + const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ queueService, pkiAlertService @@ -1921,6 +1949,8 @@ export const registerRoutes = async ( await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startAggregatedEventsJob(); await dailyResourceCleanUp.startCleanUp(); + await dailyReminderQueueService.startDailyRemindersJob(); + await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyExpiringPkiItemAlert.startSendingAlerts(); await pkiSubscriberQueue.startDailyAutoRenewalJob(); await kmsService.startService(); @@ -2031,7 +2061,8 @@ export const registerRoutes = async ( assumePrivileges: assumePrivilegeService, githubOrgSync: githubOrgSyncConfigService, folderCommit: folderCommitService, - secretScanningV2: secretScanningV2Service + secretScanningV2: secretScanningV2Service, + reminder: reminderService }); const cronJobs: CronJob[] = []; 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..8468d7305 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 @@ -51,6 +51,10 @@ import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema } from "@app/services/app-connection/databricks"; +import { + DigitalOceanConnectionListItemSchema, + SanitizedDigitalOceanConnectionSchema +} from "@app/services/app-connection/digital-ocean"; import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; @@ -71,6 +75,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 +143,9 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedZabbixConnectionSchema.options, ...SanitizedRailwayConnectionSchema.options, ...SanitizedChecklyConnectionSchema.options, - ...SanitizedSupabaseConnectionSchema.options + ...SanitizedSupabaseConnectionSchema.options, + ...SanitizedDigitalOceanConnectionSchema.options, + ...SanitizedOktaConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -175,7 +182,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ ZabbixConnectionListItemSchema, RailwayConnectionListItemSchema, ChecklyConnectionListItemSchema, - SupabaseConnectionListItemSchema + SupabaseConnectionListItemSchema, + DigitalOceanConnectionListItemSchema, + 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/digital-ocean-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/digital-ocean-connection-router.ts new file mode 100644 index 000000000..cc8498213 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/digital-ocean-connection-router.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateDigitalOceanConnectionSchema, + SanitizedDigitalOceanConnectionSchema, + UpdateDigitalOceanConnectionSchema +} from "@app/services/app-connection/digital-ocean"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDigitalOceanConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.DigitalOcean, + server, + createSchema: CreateDigitalOceanConnectionSchema, + updateSchema: UpdateDigitalOceanConnectionSchema, + sanitizedResponseSchema: SanitizedDigitalOceanConnectionSchema + }); + + // 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(), + spec: z.object({ + name: z.string() + }) + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const apps = await server.services.appConnection.digitalOcean.listApps(connectionId, req.permission); + + return { apps }; + } + }); +}; 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..6744e46b3 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -14,6 +14,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -25,6 +26,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 +74,7 @@ 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/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index c466be087..135effc4c 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -904,7 +904,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId, path: secretPath, search, - tagSlugs: tags + tagSlugs: tags, + includeTagsInSearch: true, + includeMetadataInSearch: true }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { @@ -924,7 +926,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, limit: remainingLimit, offset: adjustedOffset, - tagSlugs: tags + tagSlugs: tags, + includeTagsInSearch: true, + includeMetadataInSearch: true }) ).secrets; } @@ -1097,7 +1101,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { filters: { ...sharedFilters, tagSlugs: tags, - includeTagsInSearch: true + includeTagsInSearch: true, + includeMetadataInSearch: true } }, req.permission diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index e6aa2e83f..848bd4f31 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -42,6 +42,7 @@ import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; +import { SECRET_REMINDER_REGISTER_ROUTER_MAP } from "./reminder-routers"; import { registerSecretFolderRouter } from "./secret-folder-router"; import { registerSecretImportRouter } from "./secret-import-router"; import { registerSecretRequestsRouter } from "./secret-requests-router"; @@ -172,4 +173,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { }, { prefix: "/secret-syncs" } ); + + await server.register( + async (reminderRouter) => { + // register service specific reminder endpoints (reminders/secret) + for await (const [reminderType, router] of Object.entries(SECRET_REMINDER_REGISTER_ROUTER_MAP)) { + await reminderRouter.register(router, { prefix: `/${reminderType}` }); + } + }, + { prefix: "/reminders" } + ); }; 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/reminder-routers/index.ts b/backend/src/server/routes/v1/reminder-routers/index.ts new file mode 100644 index 000000000..863915c9c --- /dev/null +++ b/backend/src/server/routes/v1/reminder-routers/index.ts @@ -0,0 +1,8 @@ +import { ReminderType } from "@app/services/reminder/reminder-enums"; + +import { registerSecretReminderRouter } from "./secret-reminder-router"; + +export const SECRET_REMINDER_REGISTER_ROUTER_MAP: Record Promise> = + { + [ReminderType.SECRETS]: registerSecretReminderRouter + }; diff --git a/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts b/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts new file mode 100644 index 000000000..4aa68197f --- /dev/null +++ b/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts @@ -0,0 +1,154 @@ +import { z } from "zod"; + +import { RemindersSchema } from "@app/db/schemas/reminders"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerSecretReminderRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/:secretId", + method: "POST", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + secretId: z.string().uuid() + }), + body: z + .object({ + message: z.string().trim().max(1024).optional(), + repeatDays: z.number().min(1).nullable().optional(), + nextReminderDate: z.string().datetime().nullable().optional(), + recipients: z.string().array().optional() + }) + .refine((data) => { + return data.repeatDays || data.nextReminderDate; + }, "At least one of repeatDays or nextReminderDate is required"), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.reminder.createReminder({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + reminder: { + secretId: req.params.secretId, + message: req.body.message, + repeatDays: req.body.repeatDays, + nextReminderDate: req.body.nextReminderDate, + recipients: req.body.recipients + } + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_SECRET_REMINDER, + metadata: { + secretId: req.params.secretId, + message: req.body.message, + repeatDays: req.body.repeatDays, + nextReminderDate: req.body.nextReminderDate, + recipients: req.body.recipients + } + } + }); + + return { message: "Successfully created reminder" }; + } + }); + + server.route({ + url: "/:secretId", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + secretId: z.string().uuid() + }), + response: { + 200: z.object({ + reminder: RemindersSchema.extend({ + recipients: z.string().array().optional() + }) + .optional() + .nullable() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const reminder = await server.services.reminder.getReminder({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + secretId: req.params.secretId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_SECRET_REMINDER, + metadata: { + secretId: req.params.secretId + } + } + }); + return { reminder }; + } + }); + + server.route({ + url: "/:secretId", + method: "DELETE", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + secretId: z.string().uuid() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.reminder.deleteReminder({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + secretId: req.params.secretId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.DELETE_SECRET_REMINDER, + metadata: { + secretId: req.params.secretId + } + } + }); + return { message: "Successfully deleted reminder" }; + } + }); +}; 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/digital-ocean-app-platform-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/digital-ocean-app-platform-sync-router.ts new file mode 100644 index 000000000..9ffc58951 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/digital-ocean-app-platform-sync-router.ts @@ -0,0 +1,17 @@ +import { + CreateDigitalOceanAppPlatformSyncSchema, + DigitalOceanAppPlatformSyncSchema, + UpdateDigitalOceanAppPlatformSyncSchema +} from "@app/services/secret-sync/digital-ocean-app-platform"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerDigitalOceanAppPlatformSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.DigitalOceanAppPlatform, + server, + responseSchema: DigitalOceanAppPlatformSyncSchema, + createSchema: CreateDigitalOceanAppPlatformSyncSchema, + updateSchema: UpdateDigitalOceanAppPlatformSyncSchema + }); 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..03cf9d053 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -7,11 +7,13 @@ 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"; import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerDigitalOceanAppPlatformSyncRouter } from "./digital-ocean-app-platform-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; @@ -57,5 +59,7 @@ 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..833522cbb 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -32,7 +32,9 @@ export enum AppConnection { Railway = "railway", Bitbucket = "bitbucket", Checkly = "checkly", - Supabase = "supabase" + Supabase = "supabase", + DigitalOcean = "digital-ocean", + 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 54aac2001..d72fca654 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -68,6 +68,11 @@ import { getDatabricksConnectionListItem, validateDatabricksConnectionCredentials } from "./databricks"; +import { + DigitalOceanConnectionMethod, + getDigitalOceanConnectionListItem, + validateDigitalOceanConnectionCredentials +} from "./digital-ocean"; import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; @@ -92,6 +97,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"; @@ -155,7 +161,9 @@ export const listAppConnectionOptions = () => { getRailwayConnectionListItem(), getBitbucketConnectionListItem(), getChecklyConnectionListItem(), - getSupabaseConnectionListItem() + getSupabaseConnectionListItem(), + getDigitalOceanConnectionListItem(), + getOktaConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -241,7 +249,9 @@ 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.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService); @@ -280,6 +290,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case CloudflareConnectionMethod.APIToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case DigitalOceanConnectionMethod.ApiToken: + case OktaConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -367,7 +379,9 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Checkly]: platformManagedCredentialsNotSupported, - [AppConnection.Supabase]: platformManagedCredentialsNotSupported + [AppConnection.Supabase]: platformManagedCredentialsNotSupported, + [AppConnection.DigitalOcean]: 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..603d30ccd 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -34,7 +34,9 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Railway]: "Railway", [AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Checkly]: "Checkly", - [AppConnection.Supabase]: "Supabase" + [AppConnection.Supabase]: "Supabase", + [AppConnection.DigitalOcean]: "DigitalOcean App Platform", + [AppConnection.Okta]: "Okta" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -71,5 +73,7 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -273,6 +287,8 @@ export type TAppConnectionInput = { id: string } & ( | TRailwayConnectionInput | TChecklyConnectionInput | TSupabaseConnectionInput + | TDigitalOceanConnectionInput + | TOktaConnectionInput ); export type TSqlConnectionInput = @@ -321,7 +337,9 @@ export type TAppConnectionConfig = | TZabbixConnectionConfig | TRailwayConnectionConfig | TChecklyConnectionConfig - | TSupabaseConnectionConfig; + | TSupabaseConnectionConfig + | TDigitalOceanConnectionConfig + | TOktaConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -357,7 +375,9 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateZabbixConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema | TValidateChecklyConnectionCredentialsSchema - | TValidateSupabaseConnectionCredentialsSchema; + | TValidateSupabaseConnectionCredentialsSchema + | TValidateDigitalOceanCredentialsSchema + | TValidateOktaConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; 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/digital-ocean/digital-ocean-connection-constants.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-constants.ts new file mode 100644 index 000000000..60cffdbe1 --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-constants.ts @@ -0,0 +1,3 @@ +export enum DigitalOceanConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-fns.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-fns.ts new file mode 100644 index 000000000..4d66ae50f --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-fns.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; +import { z } from "zod"; + +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants"; +import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client"; +import { DigitalOceanConnectionListItemSchema } from "./digital-ocean-connection-schemas"; +import { TDigitalOceanConnectionConfig } from "./digital-ocean-connection-types"; + +export const getDigitalOceanConnectionListItem = () => { + return { + name: "Digital Ocean" as z.infer["name"], + app: AppConnection.DigitalOcean as const, + methods: Object.values(DigitalOceanConnectionMethod) + }; +}; + +export const validateDigitalOceanConnectionCredentials = async (config: TDigitalOceanConnectionConfig) => { + try { + await DigitalOceanAppPlatformPublicAPI.healthcheck(config); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + + throw new BadRequestError({ + message: "Unable to validate connection - verify credentials" + }); + } + + return config.credentials; +}; diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-public-client.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-public-client.ts new file mode 100644 index 000000000..dcf3ed911 --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-public-client.ts @@ -0,0 +1,105 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable class-methods-use-this */ +import { AxiosInstance } from "axios"; + +import { createRequestClient } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants"; +import { + TDigitalOceanApp, + TDigitalOceanConnectionConfig, + TDigitalOceanVariable +} from "./digital-ocean-connection-types"; + +class DigitalOceanAppPlatformPublicClient { + private readonly client: AxiosInstance; + + constructor() { + this.client = createRequestClient({ + baseURL: `${IntegrationUrls.DIGITAL_OCEAN_API_URL}/v2`, + headers: { + "Content-Type": "application/json", + Accept: "application/json" + } + }); + } + + async healthcheck(connection: TDigitalOceanConnectionConfig) { + switch (connection.method) { + case DigitalOceanConnectionMethod.ApiToken: + await this.getApps(connection); + break; + default: + throw new Error(`Unsupported connection method`); + } + } + + async getApps(connection: TDigitalOceanConnectionConfig) { + const response = await this.client.get<{ apps: TDigitalOceanApp[] }>(`/apps`, { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + }); + + return response.data.apps; + } + + async getApp(connection: TDigitalOceanConnectionConfig, appId: string) { + const response = await this.client.get<{ app: TDigitalOceanApp }>(`/apps/${appId}`, { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + }); + + return response.data.app; + } + + async getVariables(connection: TDigitalOceanConnectionConfig, appId: string): Promise { + const app = await this.getApp(connection, appId); + return app.spec.envs || []; + } + + async putVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) { + const response = await this.getApp(connection, appId); + + return this.client.put( + `/apps/${appId}`, + { + spec: { + ...response.spec, + envs: input + } + }, + { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + } + ); + } + + async deleteVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) { + const response = await this.getApp(connection, appId); + const existing = response.spec.envs || []; + + const variables = existing.filter((v) => input.find((i) => i.key === v.key)); + + return this.client.put( + `/apps/${appId}`, + { + spec: { + ...response.spec, + envs: variables + } + }, + { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + } + ); + } +} + +export const DigitalOceanAppPlatformPublicAPI = new DigitalOceanAppPlatformPublicClient(); diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-schemas.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-schemas.ts new file mode 100644 index 000000000..449721a3d --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-schemas.ts @@ -0,0 +1,67 @@ +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 { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants"; + +export const DigitalOceanConnectionMethodSchema = z + .nativeEnum(DigitalOceanConnectionMethod) + .describe(AppConnections.CREATE(AppConnection.DigitalOcean).method); + +export const DigitalOceanConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .max(255) + .describe(AppConnections.CREDENTIALS.DIGITAL_OCEAN_APP_PLATFORM.apiToken) +}); + +const BaseDigitalOceanConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.DigitalOcean) +}); + +export const DigitalOceanConnectionSchema = BaseDigitalOceanConnectionSchema.extend({ + method: DigitalOceanConnectionMethodSchema, + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedDigitalOceanConnectionSchema = z.discriminatedUnion("method", [ + BaseDigitalOceanConnectionSchema.extend({ + method: DigitalOceanConnectionMethodSchema, + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: DigitalOceanConnectionMethodSchema, + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.DigitalOcean).credentials + ) + }) +]); + +export const CreateDigitalOceanConnectionSchema = ValidateDigitalOceanConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.DigitalOcean) +); + +export const UpdateDigitalOceanConnectionSchema = z + .object({ + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.DigitalOcean).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean)); + +export const DigitalOceanConnectionListItemSchema = z.object({ + name: z.literal("Digital Ocean"), + app: z.literal(AppConnection.DigitalOcean), + methods: z.nativeEnum(DigitalOceanConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts new file mode 100644 index 000000000..97d4f700f --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts @@ -0,0 +1,29 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client"; +import { TDigitalOceanConnection } from "./digital-ocean-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const digitalOceanAppPlatformConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const connection = await getAppConnection(AppConnection.DigitalOcean, connectionId, actor); + try { + const apps = await DigitalOceanAppPlatformPublicAPI.getApps(connection); + return apps; + } catch (error) { + logger.error(error, "Failed to list apps on Digital Ocean"); + return []; + } + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-types.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-types.ts new file mode 100644 index 000000000..cc6b4c1ef --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-types.ts @@ -0,0 +1,42 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateDigitalOceanConnectionSchema, + DigitalOceanConnectionSchema, + ValidateDigitalOceanConnectionCredentialsSchema +} from "./digital-ocean-connection-schemas"; + +export type TDigitalOceanConnection = z.infer; + +export type TDigitalOceanConnectionInput = z.infer & { + app: AppConnection.DigitalOcean; +}; + +export type TValidateDigitalOceanCredentialsSchema = typeof ValidateDigitalOceanConnectionCredentialsSchema; + +export type TDigitalOceanConnectionConfig = DiscriminativePick< + TDigitalOceanConnection, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TDigitalOceanVariable = { + key: string; + value: string; + type: "SECRET" | "GENERAL"; +}; + +export type TDigitalOceanApp = { + id: string; + spec: { + name: string; + services: Array<{ + name: string; + }>; + envs?: TDigitalOceanVariable[]; + }; +}; diff --git a/backend/src/services/app-connection/digital-ocean/index.ts b/backend/src/services/app-connection/digital-ocean/index.ts new file mode 100644 index 000000000..8c8d483b7 --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/index.ts @@ -0,0 +1,4 @@ +export * from "./digital-ocean-connection-constants"; +export * from "./digital-ocean-connection-fns"; +export * from "./digital-ocean-connection-schemas"; +export * from "./digital-ocean-connection-types"; 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/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/microsoft-teams/microsoft-teams-fns.ts b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts index 8734b8459..38fc99819 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts @@ -402,7 +402,7 @@ export const buildTeamsPayload = (notification: TNotification) => { { type: "Action.OpenUrl", title: "View request in Infisical", - url: `${appCfg.SITE_URL}/projects/${payload.projectId}/secret-manager/approval?requestId=${payload.requestId}` + url: `${appCfg.SITE_URL}/projects/secret-management/${payload.projectId}/approval?requestId=${payload.requestId}` } ] }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index eaf869cae..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, @@ -46,7 +47,7 @@ import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; -import { QueueName, TQueueServiceFactory } from "@app/queue"; +import { QueueName } from "@app/queue"; import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -64,6 +65,7 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; +import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TSecretDALFactory } from "../secret/secret-dal"; import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -131,8 +133,8 @@ type TOrgServiceFactoryDep = { projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; - queueService: Pick; loginService: Pick; + reminderService: Pick; }; export type TOrgServiceFactory = ReturnType; @@ -164,8 +166,8 @@ export const orgServiceFactory = ({ projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, - queueService, - loginService + loginService, + reminderService }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -608,7 +610,7 @@ export const orgServiceFactory = ({ await fnDeleteProjectSecretReminders(project.id, { secretDAL, secretV2BridgeDAL, - queueService, + reminderService, projectBotService, folderDAL }); @@ -980,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 de35c11f4..27925c4b9 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"; @@ -32,7 +39,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; -import { TQueueServiceFactory } from "@app/queue"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -60,6 +66,7 @@ import { TProjectMembershipDALFactory } from "../project-membership/project-memb import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { getPredefinedRoles } from "../project-role/project-role-fns"; +import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TSecretDALFactory } from "../secret/secret-dal"; import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -162,7 +169,6 @@ type TProjectServiceFactoryDep = { permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; - queueService: Pick; smtpService: Pick; orgDAL: Pick; keyStore: Pick; @@ -179,6 +185,7 @@ type TProjectServiceFactoryDep = { | "createCipherPairWithDataKey" >; projectTemplateService: TProjectTemplateServiceFactory; + reminderService: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -191,7 +198,6 @@ export const projectServiceFactory = ({ projectQueue, projectKeyDAL, permissionService, - queueService, projectBotService, orgDAL, userDAL, @@ -226,7 +232,8 @@ export const projectServiceFactory = ({ microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, - smtpService + smtpService, + reminderService }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -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); @@ -558,7 +574,7 @@ export const projectServiceFactory = ({ await fnDeleteProjectSecretReminders(project.id, { secretDAL, secretV2BridgeDAL, - queueService, + reminderService, projectBotService, folderDAL }); @@ -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 }) @@ -1890,6 +1948,13 @@ export const projectServiceFactory = ({ const userDetails = await userDAL.findById(permission.id); const appCfg = getConfig(); + let projectTypeUrl = project.type; + if (project.type === ProjectType.SecretManager) { + projectTypeUrl = "secret-management"; + } else if (project.type === ProjectType.CertificateManager) { + projectTypeUrl = "cert-management"; + } + await smtpService.sendMail({ template: SmtpTemplates.ProjectAccessRequest, recipients: filteredProjectMembers, @@ -1900,7 +1965,7 @@ export const projectServiceFactory = ({ projectName: project?.name, orgName: org?.name, note: comment, - callback_url: `${appCfg.SITE_URL}/${project.type}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}` + callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}` } }); }; 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/reminder-recipients/reminder-recipient-dal.ts b/backend/src/services/reminder-recipients/reminder-recipient-dal.ts new file mode 100644 index 000000000..67988d4db --- /dev/null +++ b/backend/src/services/reminder-recipients/reminder-recipient-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TReminderRecipientDALFactory = ReturnType; + +export const reminderRecipientDALFactory = (db: TDbClient) => { + const reminderRecipientOrm = ormify(db, TableName.ReminderRecipient); + + return { ...reminderRecipientOrm }; +}; diff --git a/backend/src/services/reminder/reminder-dal.ts b/backend/src/services/reminder/reminder-dal.ts new file mode 100644 index 000000000..0e5d7b895 --- /dev/null +++ b/backend/src/services/reminder/reminder-dal.ts @@ -0,0 +1,133 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { + TableName, + TOrganizations, + TProjectEnvironments, + TProjects, + TSecretFolders, + TSecretsV2, + TUsers +} from "@app/db/schemas"; +import { RemindersSchema } from "@app/db/schemas/reminders"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +export type TReminderDALFactory = ReturnType; + +export const reminderDALFactory = (db: TDbClient) => { + const reminderOrm = ormify(db, TableName.Reminder); + + const getTodayDateRange = () => { + const today = new Date(); + const year = today.getUTCFullYear(); + const month = today.getUTCMonth(); + const date = today.getUTCDate(); + + // Start of day: 00:00:00.000 UTC + const startOfDay = new Date(Date.UTC(year, month, date, 0, 0, 0, 0)); + + // End of day: 23:59:59.999 UTC + const endOfDay = new Date(Date.UTC(year, month, date, 23, 59, 59, 999)); + + return { + startOfDay, + endOfDay + }; + }; + + const findSecretDailyReminders = async (tx?: Knex) => { + const { startOfDay, endOfDay } = getTodayDateRange(); + + const rawReminders = await (tx || db)(TableName.Reminder) + .whereBetween("nextReminderDate", [startOfDay, endOfDay]) + .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) + .leftJoin(TableName.Users, `${TableName.ReminderRecipient}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.SecretV2, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretFolder, + `${TableName.SecretV2}.folderId`, + `${TableName.SecretFolder}.id` + ) + .leftJoin( + TableName.Environment, + `${TableName.SecretFolder}.envId`, + `${TableName.Environment}.id` + ) + .leftJoin(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .leftJoin(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) + .select(selectAllTableCols(TableName.Reminder)) + .select(db.ref("email").withSchema(TableName.Users)) + .select(db.ref("name").withSchema(TableName.Project).as("projectName")) + .select(db.ref("id").withSchema(TableName.Project).as("projectId")) + .select(db.ref("name").withSchema(TableName.Organization).as("organizationName")); + + const reminders = sqlNestRelationships({ + data: rawReminders, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...RemindersSchema.parse(el), + projectName: el.projectName, + projectId: el.projectId, + organizationName: el.organizationName + }), + childrenMapper: [ + { + key: "email", + label: "recipients" as const, + mapper: ({ email }) => ({ + email + }) + } + ] + }); + return reminders; + }; + + const findUpcomingReminders = async (daysAhead: number = 7, tx?: Knex) => { + const { startOfDay } = getTodayDateRange(); + const futureDate = new Date(startOfDay); + futureDate.setDate(futureDate.getDate() + daysAhead); + + const reminders = await (tx || db)(TableName.Reminder) + .where("nextReminderDate", ">=", startOfDay) + .where("nextReminderDate", "<=", futureDate) + .orderBy("nextReminderDate", "asc") + .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) + .select(selectAllTableCols(TableName.Reminder)) + .select(db.ref("userId").withSchema(TableName.ReminderRecipient)); + return reminders; + }; + + const findSecretReminder = async (secretId: string, tx?: Knex) => { + const rawReminders = await (tx || db)(TableName.Reminder) + .where(`${TableName.Reminder}.secretId`, secretId) + .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) + .select(selectAllTableCols(TableName.Reminder)) + .select(db.ref("userId").withSchema(TableName.ReminderRecipient)); + const reminders = sqlNestRelationships({ + data: rawReminders, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...RemindersSchema.parse(el) + }), + childrenMapper: [ + { + key: "userId", + label: "recipients" as const, + mapper: ({ userId }) => userId + } + ] + }); + return reminders[0] || null; + }; + + return { + ...reminderOrm, + findSecretDailyReminders, + findUpcomingReminders, + findSecretReminder + }; +}; diff --git a/backend/src/services/reminder/reminder-enums.ts b/backend/src/services/reminder/reminder-enums.ts new file mode 100644 index 000000000..c6650b593 --- /dev/null +++ b/backend/src/services/reminder/reminder-enums.ts @@ -0,0 +1,3 @@ +export enum ReminderType { + SECRETS = "secrets" +} diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts new file mode 100644 index 000000000..4e31c8a6d --- /dev/null +++ b/backend/src/services/reminder/reminder-queue.ts @@ -0,0 +1,196 @@ +/* eslint-disable no-await-in-loop */ +import RE2 from "re2"; + +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { TReminderServiceFactory } from "./reminder-types"; + +type TDailyReminderQueueServiceFactoryDep = { + reminderService: TReminderServiceFactory; + queueService: TQueueServiceFactory; + secretDAL: Pick; + secretReminderRecipientsDAL: Pick; +}; + +export type TDailyReminderQueueServiceFactory = ReturnType; + +const uuidRegex = new RE2(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); + +export const dailyReminderQueueServiceFactory = ({ + reminderService, + queueService, + secretDAL, + secretReminderRecipientsDAL +}: TDailyReminderQueueServiceFactoryDep) => { + queueService.start(QueueName.DailyReminders, async () => { + logger.info(`${QueueName.DailyReminders}: queue task started`); + await reminderService.sendDailyReminders(); + logger.info(`${QueueName.DailyReminders}: queue task completed`); + }); + + queueService.start(QueueName.SecretReminderMigration, async () => { + const REMINDER_PRUNE_BATCH_SIZE = 5_000; + const MAX_RETRY_ON_FAILURE = 3; + let numberOfRetryOnFailure = 0; + let deletedReminderCount = 0; + + logger.info(`${QueueName.SecretReminderMigration}: queue task started`); + try { + const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder); + const delayedJobs = await queueService.getDelayedJobs(QueueName.SecretReminder); + logger.info(`${QueueName.SecretReminderMigration}: found ${repeatableJobs.length} secret reminder jobs`); + + const reminderJobs = repeatableJobs + .map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key })) + .filter(Boolean); + const reminderDelayedJobs = delayedJobs.reduce((map, job) => { + const match = uuidRegex.exec(job.repeatJobKey || ""); + if (match) { + map.set(match[0], { + timestamp: job.timestamp, + delay: job.delay, + data: job.data + }); + } + return map; + }, new Map()); + if (reminderJobs.length === 0) { + logger.info(`${QueueName.SecretReminderMigration}: no reminder jobs found`); + return; + } + + for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) { + try { + const batch = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE); + const batchIds = batch.map((job) => job.secretId); + + // Find existing secrets with pagination + // eslint-disable-next-line no-await-in-loop + const secrets = await secretDAL.findSecretsWithReminderRecipients(batchIds, REMINDER_PRUNE_BATCH_SIZE); + const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays); + + const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id)); + + // Find IDs that don't exist in either table + const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId)); + + // Delete reminders for non-existent secrets + for (const secretId of secretIdsNotFound) { + const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey; + + if (jobKey) { + // eslint-disable-next-line no-await-in-loop + await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey); + deletedReminderCount += 1; + } + } + + for (const secretId of foundSecretIds) { + const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey; + + if (jobKey) { + await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey); + deletedReminderCount += 1; + } + } + + await secretDAL.transaction(async (tx) => { + await reminderService.batchCreateReminders( + secretsWithReminder.map((secret) => { + const delayedJob = reminderDelayedJobs.get(secret.id); + const projectId = (delayedJob?.data as { projectId?: string })?.projectId; + const nextDate = delayedJob ? new Date(delayedJob.timestamp + delayedJob.delay) : undefined; + return { + secretId: secret.id, + message: secret.reminderNote, + repeatDays: secret.reminderRepeatDays, + nextReminderDate: nextDate, + recipients: secret.recipients || [], + projectId + }; + }), + tx + ); + + await secretReminderRecipientsDAL.delete({ $in: { secretId: secretsWithReminder.map((s) => s.id) } }, tx); + }); + + numberOfRetryOnFailure = 0; + } catch (error) { + numberOfRetryOnFailure += 1; + logger.error(error, `Failed to process batch at offset ${offset}`); + + if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) { + break; + } + + // Retry the current batch + offset -= REMINDER_PRUNE_BATCH_SIZE; + + // eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure)); + } + + // Small delay between batches + // eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } catch (error) { + logger.error(error, "Failed to complete secret reminder pruning"); + } finally { + logger.info( + `${QueueName.SecretReminderMigration}: secret reminders completed. Deleted ${deletedReminderCount} reminders` + ); + } + }); + + // we do a repeat cron job in utc timezone at 12 Midnight each day + const startDailyRemindersJob = async () => { + // clear previous job + await queueService.stopRepeatableJob( + QueueName.DailyReminders, + QueueJobs.DailyReminders, + { pattern: "0 0 * * *", utc: true }, + QueueName.DailyReminders // just a job id + ); + + await queueService.queue(QueueName.DailyReminders, QueueJobs.DailyReminders, undefined, { + delay: 5000, + jobId: QueueName.DailyReminders, + repeat: { pattern: "0 0 * * *", utc: true } + }); + }; + + // TODO: remove once all the old reminders in queues are migrated + const startSecretReminderMigrationJob = async () => { + // clear previous job + await queueService.stopRepeatableJob( + QueueName.SecretReminderMigration, + QueueJobs.SecretReminderMigration, + { pattern: "0 */1 * * *", utc: true }, + QueueName.SecretReminderMigration // just a job id + ); + + await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, { + delay: 5000, + jobId: QueueName.SecretReminderMigration, + repeat: { pattern: "0 */1 * * *", utc: true } + }); + }; + + queueService.listen(QueueName.DailyReminders, "failed", (_, err) => { + logger.error(err, `${QueueName.DailyReminders}: daily reminder processing failed`); + }); + + queueService.listen(QueueName.SecretReminderMigration, "failed", (_, err) => { + logger.error(err, `${QueueName.SecretReminderMigration}: secret reminder migration failed`); + }); + + return { + startDailyRemindersJob, + startSecretReminderMigrationJob + }; +}; diff --git a/backend/src/services/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts new file mode 100644 index 000000000..ddccbbf62 --- /dev/null +++ b/backend/src/services/reminder/reminder-service.ts @@ -0,0 +1,359 @@ +/* eslint-disable no-await-in-loop */ +import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; + +import { ActionProjectType, TableName } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TReminderRecipientDALFactory } from "../reminder-recipients/reminder-recipient-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TReminderDALFactory } from "./reminder-dal"; +import { TBatchCreateReminderDTO, TCreateReminderDTO, TReminderServiceFactory } from "./reminder-types"; + +type TReminderServiceFactoryDep = { + reminderDAL: TReminderDALFactory; + reminderRecipientDAL: TReminderRecipientDALFactory; + smtpService: TSmtpService; + projectMembershipDAL: Pick; + permissionService: Pick; + secretV2BridgeDAL: Pick; +}; + +export const reminderServiceFactory = ({ + reminderDAL, + reminderRecipientDAL, + smtpService, + projectMembershipDAL, + permissionService, + secretV2BridgeDAL +}: TReminderServiceFactoryDep): TReminderServiceFactory => { + const $addDays = (days: number, fromDate: Date = new Date()): Date => { + const result = new Date(fromDate); + result.setDate(result.getDate() + days); + return result; + }; + + const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise => { + if (!newRecipients || newRecipients.length === 0) { + // If no recipients provided, remove all existing recipients + await reminderRecipientDAL.deleteById(reminderId); + return; + } + + // Remove duplicates from input + const uniqueRecipients = [...new Set(newRecipients)]; + + // Get existing recipients + const existingRecipients = await reminderRecipientDAL.find({ reminderId }); + const existingUserIds = new Set(existingRecipients.map((r) => r.userId)); + const newUserIds = new Set(uniqueRecipients); + + // Find recipients to add and remove + const recipientsToAdd = uniqueRecipients.filter((userId) => !existingUserIds.has(userId)); + const recipientsToRemove = existingRecipients.filter((r) => !newUserIds.has(r.userId)); + + // Perform database operations + if (recipientsToRemove.length > 0) { + await reminderRecipientDAL.delete({ $in: { id: recipientsToRemove.map((r) => r.id) } }); + } + + if (recipientsToAdd.length > 0) { + await reminderRecipientDAL.insertMany( + recipientsToAdd.map((userId) => ({ + reminderId, + userId + })) + ); + } + }; + + const createReminderInternal: TReminderServiceFactory["createReminderInternal"] = async ({ + secretId, + message, + repeatDays, + nextReminderDate: nextReminderDateInput, + recipients, + projectId + }: { + secretId?: string; + message?: string | null; + repeatDays?: number | null; + nextReminderDate?: string | null; + recipients?: string[] | null; + projectId: string; + }) => { + if (!secretId) { + throw new BadRequestError({ message: "secretId is required" }); + } + let nextReminderDate; + if (nextReminderDateInput) { + nextReminderDate = new Date(nextReminderDateInput); + } + + if (repeatDays && repeatDays > 0) { + nextReminderDate = $addDays(repeatDays); + } + + if (!nextReminderDate) { + throw new BadRequestError({ message: "repeatDays must be a positive number" }); + } + + const existingReminder = await reminderDAL.findOne({ secretId }); + let reminderId: string; + + if (existingReminder) { + // Update existing reminder + await reminderDAL.updateById(existingReminder.id, { + message, + repeatDays, + nextReminderDate + }); + reminderId = existingReminder.id; + } else { + // Create new reminder + const newReminder = await reminderDAL.create({ + secretId, + message, + repeatDays, + nextReminderDate + }); + reminderId = newReminder.id; + } + + // Manage recipients (add/update/delete as needed) + await $manageReminderRecipients(reminderId, recipients); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + return { id: reminderId, created: !existingReminder }; + }; + + const createReminder: TReminderServiceFactory["createReminder"] = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + reminder + }: TCreateReminderDTO) => { + const secret = await secretV2BridgeDAL.findOneWithTags({ [`${TableName.SecretV2}.id` as "id"]: reminder.secretId }); + if (!secret) { + throw new BadRequestError({ message: `Secret ${reminder.secretId} not found` }); + } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: secret.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionSecretActions.Edit, ProjectPermissionSub.Secrets); + + const response = await createReminderInternal({ + ...reminder, + projectId: secret.projectId + }); + return response; + }; + + const getReminder: TReminderServiceFactory["getReminder"] = async ({ + secretId, + actor, + actorId, + actorOrgId, + actorAuthMethod + }: { + secretId: string; + actor: ActorType; + actorId: string; + actorOrgId: string; + actorAuthMethod: ActorAuthMethod; + }) => { + const secret = await secretV2BridgeDAL.findOneWithTags({ [`${TableName.SecretV2}.id` as "id"]: secretId }); + if (!secret) { + throw new BadRequestError({ message: `Secret ${secretId} not found` }); + } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: secret.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); + const reminder = await reminderDAL.findSecretReminder(secretId); + return reminder; + }; + + const sendDailyReminders: TReminderServiceFactory["sendDailyReminders"] = async () => { + const remindersToSend = await reminderDAL.findSecretDailyReminders(); + + for (const reminder of remindersToSend) { + try { + await reminderDAL.transaction(async (tx) => { + const recipients: string[] = reminder.recipients + .map((r) => r.email) + .filter((email): email is string => Boolean(email)); + if (recipients.length === 0) { + const members = await projectMembershipDAL.findAllProjectMembers(reminder.projectId); + recipients.push(...members.map((m) => m.user.email).filter((email): email is string => Boolean(email))); + } + await smtpService.sendMail({ + template: SmtpTemplates.SecretReminder, + subjectLine: "Infisical secret reminder", + recipients, + substitutions: { + reminderNote: reminder.message || "", + projectName: reminder.projectName || "", + organizationName: reminder.organizationName || "" + } + }); + if (reminder.repeatDays) { + await reminderDAL.updateById(reminder.id, { nextReminderDate: $addDays(reminder.repeatDays) }, tx); + } else { + await reminderDAL.deleteById(reminder.id, tx); + } + }); + } catch (error) { + logger.error( + error, + `Failed to send reminder to recipients ${reminder.recipients.map((r) => r.email).join(", ")}` + ); + } + } + }; + + const deleteReminder: TReminderServiceFactory["deleteReminder"] = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + secretId + }: { + actor: ActorType; + actorId: string; + actorOrgId: string; + actorAuthMethod: ActorAuthMethod; + secretId: string; + }) => { + const secret = await secretV2BridgeDAL.findOneWithTags({ [`${TableName.SecretV2}.id` as "id"]: secretId }); + if (!secret) { + throw new BadRequestError({ message: `Secret ${secretId} not found` }); + } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: secret.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionSecretActions.Edit, ProjectPermissionSub.Secrets); + await reminderDAL.delete({ secretId }); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secret.projectId); + }; + + const deleteReminderBySecretId: TReminderServiceFactory["deleteReminderBySecretId"] = async ( + secretId: string, + projectId: string, + tx?: Knex + ) => { + await reminderDAL.delete({ secretId }, tx); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + }; + + const batchCreateReminders: TReminderServiceFactory["batchCreateReminders"] = async ( + remindersData: TBatchCreateReminderDTO, + tx?: Knex + ) => { + if (!remindersData || remindersData.length === 0) { + return { created: 0, reminderIds: [] }; + } + + const processedReminders = remindersData.map( + ({ secretId, message, repeatDays, nextReminderDate: nextReminderDateInput, recipients, projectId }) => { + let nextReminderDate; + if (nextReminderDateInput) { + nextReminderDate = new Date(nextReminderDateInput); + } + + if (repeatDays && repeatDays > 0 && !nextReminderDate) { + nextReminderDate = $addDays(repeatDays); + } + + if (!nextReminderDate) { + throw new BadRequestError({ + message: `repeatDays must be a positive number for secretId: ${secretId}` + }); + } + + return { + secretId, + message, + repeatDays, + nextReminderDate, + recipients: recipients ? [...new Set(recipients)] : [], + projectId + }; + } + ); + + const newReminders = await reminderDAL.insertMany( + processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, projectId }) => ({ + secretId, + message, + repeatDays, + nextReminderDate, + projectId + })), + tx + ); + + const allRecipientInserts: Array<{ reminderId: string; userId: string }> = []; + + newReminders.forEach((reminder, index) => { + const { recipients } = processedReminders[index]; + if (recipients && recipients.length > 0) { + recipients.forEach((userId) => { + allRecipientInserts.push({ + reminderId: reminder.id, + userId + }); + }); + } + }); + + if (allRecipientInserts.length > 0) { + await reminderRecipientDAL.insertMany(allRecipientInserts, tx); + } + + const projectIds = new Set(processedReminders.map((r) => r.projectId).filter((id): id is string => Boolean(id))); + for (const projectId of projectIds) { + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + } + + return { + created: newReminders.length, + reminderIds: newReminders.map((r) => r.id) + }; + }; + + return { + createReminder, + getReminder, + sendDailyReminders, + deleteReminder, + deleteReminderBySecretId, + batchCreateReminders, + createReminderInternal + }; +}; diff --git a/backend/src/services/reminder/reminder-types.ts b/backend/src/services/reminder/reminder-types.ts new file mode 100644 index 000000000..1f6a53ac7 --- /dev/null +++ b/backend/src/services/reminder/reminder-types.ts @@ -0,0 +1,102 @@ +import { Knex } from "knex"; + +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; + +export type TReminder = { + id: string; + secretId?: string | null; + message?: string | null; + repeatDays?: number | null; + nextReminderDate: Date; + createdAt: Date; + updatedAt: Date; +}; + +export type TCreateReminderDTO = { + actor: ActorType; + actorId: string; + actorOrgId: string; + actorAuthMethod: ActorAuthMethod; + reminder: { + secretId?: string; + message?: string | null; + repeatDays?: number | null; + nextReminderDate?: string | null; + recipients?: string[] | null; + }; +}; + +export type TBatchCreateReminderDTO = { + secretId: string; + message?: string | null; + repeatDays?: number | null; + nextReminderDate?: string | Date | null; + recipients?: string[] | null; + projectId?: string; +}[]; + +export interface TReminderServiceFactory { + createReminder: ({ actor, actorId, actorOrgId, actorAuthMethod, reminder }: TCreateReminderDTO) => Promise<{ + id: string; + created: boolean; + }>; + + getReminder: ({ + secretId, + actor, + actorId, + actorOrgId, + actorAuthMethod + }: { + secretId: string; + actor: ActorType; + actorId: string; + actorOrgId: string; + actorAuthMethod: ActorAuthMethod; + }) => Promise<(TReminder & { recipients: string[] }) | null>; + + sendDailyReminders: () => Promise; + + deleteReminder: ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + secretId + }: { + actor: ActorType; + actorId: string; + actorOrgId: string; + actorAuthMethod: ActorAuthMethod; + secretId: string; + }) => Promise; + + deleteReminderBySecretId: (secretId: string, projectId: string, tx?: Knex) => Promise; + + batchCreateReminders: ( + remindersData: TBatchCreateReminderDTO, + tx?: Knex + ) => Promise<{ + created: number; + reminderIds: string[]; + }>; + + createReminderInternal: ({ + secretId, + message, + repeatDays, + nextReminderDate, + recipients, + projectId + }: { + secretId?: string; + message?: string | null; + repeatDays?: number | null; + nextReminderDate?: string | null; + recipients?: string[] | null; + projectId: string; + }) => Promise<{ + id: string; + created: boolean; + }>; +} diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 39926488f..bfaef5708 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -6,7 +6,6 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; import { TOrgServiceFactory } from "../org/org-service"; -import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; @@ -19,7 +18,6 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { identityUniversalAuthClientSecretDAL: Pick; secretVersionDAL: Pick; secretVersionV2DAL: Pick; - secretDAL: Pick; secretFolderVersionDAL: Pick; snapshotDAL: Pick; secretSharingDAL: Pick; @@ -36,7 +34,6 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ snapshotDAL, secretVersionDAL, secretFolderVersionDAL, - secretDAL, identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL, @@ -46,7 +43,6 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ }: TDailyResourceCleanUpQueueServiceFactoryDep) => { queueService.start(QueueName.DailyResourceCleanUp, async () => { logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); - await secretDAL.pruneSecretReminders(queueService); await identityAccessTokenDAL.removeExpiredTokens(); await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets(); 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/digital-ocean-app-platform/digital-ocean-app-platform-sync-constants.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-constants.ts new file mode 100644 index 000000000..22dc48b3f --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-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 DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Digital Ocean App Platform" as const, + destination: SecretSync.DigitalOceanAppPlatform, + connection: AppConnection.DigitalOcean, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-fns.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-fns.ts new file mode 100644 index 000000000..a2474fd2c --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-fns.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-continue */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean"; +import { DigitalOceanAppPlatformPublicAPI } from "@app/services/app-connection/digital-ocean/digital-ocean-connection-public-client"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; +import { TSecretMap } from "../secret-sync-types"; +import { TDigitalOceanAppPlatformSyncWithCredentials } from "./digital-ocean-app-platform-sync-types"; + +export const DigitalOceanAppPlatformSyncFns = { + async getSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials) { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + async syncSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) { + const { + environment, + syncOptions: { disableSecretDeletion, keySchema } + } = secretSync; + + const config = secretSync.destinationConfig; + + const existing = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId); + + const variables: Record = Object.fromEntries(existing.map((v) => [v.key, v])); + + for (const [key, value] of Object.entries(secretMap)) { + variables[key] = { + key, + value: value.value, + type: "SECRET" + } as TDigitalOceanVariable; + } + + if (!disableSecretDeletion) { + for (const v of existing) { + if (!matchesSchema(v.key, environment?.slug || "", keySchema)) continue; + if (!(v.key in secretMap)) { + delete variables[v.key]; + } + } + } + + try { + const vars = Object.values(variables); + await DigitalOceanAppPlatformPublicAPI.putVariables(secretSync.connection, config.appId, ...vars); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + + async removeSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) { + const config = secretSync.destinationConfig; + + try { + const existingSecrets = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId); + + const vars = Object.entries(existingSecrets) + .map(([key, v]) => { + if (!(key in secretMap)) return; + + return { + key, + value: v.value, + type: "SECRET" + } as TDigitalOceanVariable; + }) + .filter(Boolean) as TDigitalOceanVariable[]; + + await DigitalOceanAppPlatformPublicAPI.deleteVariables(secretSync.connection, config.appId, ...vars); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + } +}; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts new file mode 100644 index 000000000..09b943d16 --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +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 DigitalOceanAppPlatformSyncDestinationConfigSchema = z.object({ + appId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"), + appName: z.string().min(1, "Account Name is required").max(255, "Account Name must be less than 255 characters") +}); + +const DigitalOceanAppPlatformSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const DigitalOceanAppPlatformSyncSchema = BaseSecretSyncSchema( + SecretSync.DigitalOceanAppPlatform, + DigitalOceanAppPlatformSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.DigitalOceanAppPlatform), + destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema +}); + +export const CreateDigitalOceanAppPlatformSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.DigitalOceanAppPlatform, + DigitalOceanAppPlatformSyncOptionsConfig +).extend({ + destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema +}); + +export const UpdateDigitalOceanAppPlatformSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.DigitalOceanAppPlatform, + DigitalOceanAppPlatformSyncOptionsConfig +).extend({ + destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema.optional() +}); + +export const DigitalOceanAppPlatformSyncListItemSchema = z.object({ + name: z.literal("Digital Ocean App Platform"), + connection: z.literal(AppConnection.DigitalOcean), + destination: z.literal(SecretSync.DigitalOceanAppPlatform), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-types.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-types.ts new file mode 100644 index 000000000..87832079b --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-types.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +import { TDigitalOceanConnection, TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean"; + +import { + CreateDigitalOceanAppPlatformSyncSchema, + DigitalOceanAppPlatformSyncListItemSchema, + DigitalOceanAppPlatformSyncSchema +} from "./digital-ocean-app-platform-sync-schemas"; + +export type TDigitalOceanAppPlatformSyncListItem = z.infer; + +export type TDigitalOceanAppPlatformSync = z.infer; + +export type TDigitalOceanAppPlatformSyncInput = z.infer; + +export type TDigitalOceanAppPlatformSyncWithCredentials = TDigitalOceanAppPlatformSync & { + connection: TDigitalOceanConnection; +}; + +export type TDigitalOceanAppPlatformSecret = TDigitalOceanVariable & { + type: "SECRET"; +}; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/index.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/index.ts new file mode 100644 index 000000000..9af720b23 --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/index.ts @@ -0,0 +1,4 @@ +export * from "./digital-ocean-app-platform-sync-constants"; +export * from "./digital-ocean-app-platform-sync-fns"; +export * from "./digital-ocean-app-platform-sync-schemas"; +export * from "./digital-ocean-app-platform-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 8d08e4d82..55859200a 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -25,7 +25,9 @@ export enum SecretSync { Supabase = "supabase", Zabbix = "zabbix", Railway = "railway", - Checkly = "checkly" + Checkly = "checkly", + DigitalOceanAppPlatform = "digital-ocean-app-platform", + 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..caa6ddcee 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -28,12 +28,17 @@ 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"; import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants"; import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"; import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers"; +import { + DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION, + DigitalOceanAppPlatformSyncFns +} from "./digital-ocean-app-platform"; import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -80,7 +85,9 @@ 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.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION, + [SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -258,6 +265,10 @@ export const SecretSyncFns = { return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.DigitalOceanAppPlatform: + return DigitalOceanAppPlatformSyncFns.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 +376,12 @@ export const SecretSyncFns = { case SecretSync.Supabase: secretMap = await SupabaseSyncFns.getSecrets(secretSync); break; + case SecretSync.DigitalOceanAppPlatform: + secretMap = await DigitalOceanAppPlatformSyncFns.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 +469,10 @@ export const SecretSyncFns = { return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.DigitalOceanAppPlatform: + return DigitalOceanAppPlatformSyncFns.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..2f2b3c427 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -28,7 +28,9 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Supabase]: "Supabase", [SecretSync.Zabbix]: "Zabbix", [SecretSync.Railway]: "Railway", - [SecretSync.Checkly]: "Checkly" + [SecretSync.Checkly]: "Checkly", + [SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform", + [SecretSync.Bitbucket]: "Bitbucket" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -58,7 +60,9 @@ 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.DigitalOceanAppPlatform]: AppConnection.DigitalOcean, + [SecretSync.Bitbucket]: AppConnection.Bitbucket }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -88,5 +92,7 @@ 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.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular, + [SecretSync.Bitbucket]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 8f5a2e806..5d788ea88 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -869,7 +869,7 @@ export const secretSyncQueueFactory = ({ secretPath: folder?.path, environment: environment?.name, projectName: project.name, - syncUrl: `${appCfg.SITE_URL}/projects/${projectId}/secret-manager/integrations/secret-syncs/${destination}/${secretSync.id}` + syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}` } }); }; 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..a844fbe48 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, @@ -90,6 +96,11 @@ import { TCloudflareWorkersSyncListItem, TCloudflareWorkersSyncWithCredentials } from "./cloudflare-workers"; +import { + TDigitalOceanAppPlatformSyncInput, + TDigitalOceanAppPlatformSyncListItem, + TDigitalOceanAppPlatformSyncWithCredentials +} from "./digital-ocean-app-platform/digital-ocean-app-platform-sync-types"; import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; @@ -166,7 +177,8 @@ export type TSecretSync = | TZabbixSync | TRailwaySync | TChecklySync - | TSupabaseSync; + | TSupabaseSync + | TBitbucketSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -195,7 +207,9 @@ export type TSecretSyncWithCredentials = | TZabbixSyncWithCredentials | TRailwaySyncWithCredentials | TChecklySyncWithCredentials - | TSupabaseSyncWithCredentials; + | TSupabaseSyncWithCredentials + | TDigitalOceanAppPlatformSyncWithCredentials + | TBitbucketSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -224,7 +238,9 @@ export type TSecretSyncInput = | TZabbixSyncInput | TRailwaySyncInput | TChecklySyncInput - | TSupabaseSyncInput; + | TSupabaseSyncInput + | TDigitalOceanAppPlatformSyncInput + | TBitbucketSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -253,7 +269,9 @@ export type TSecretSyncListItem = | TZabbixSyncListItem | TRailwaySyncListItem | TChecklySyncListItem - | TSupabaseSyncListItem; + | TSupabaseSyncListItem + | TDigitalOceanAppPlatformSyncListItem + | 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-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index cd2773172..4cbd1d783 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -415,6 +415,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { filters?: { search?: string; tagSlugs?: string[]; + includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; } ) => { try { @@ -433,17 +435,27 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { .whereIn("folderId", folderIds) .where((bd) => { if (filters?.search) { - void bd.whereILike("key", `%${filters?.search}%`); + void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); + if (filters?.includeTagsInSearch) { + void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); + } + if (filters?.includeMetadataInSearch) { + void bd + .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`) + .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`); + } } }) .where((bd) => { - void bd.whereNull("userId").orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) - .countDistinct("key"); + .countDistinct(`${TableName.SecretV2}.key`); // only need to join tags if filtering by tag slugs const slugs = filters?.tagSlugs?.filter(Boolean); - if (slugs && slugs.length > 0) { + if ((slugs && slugs.length > 0) || filters?.includeTagsInSearch) { void query .leftJoin( TableName.SecretV2JnTag, @@ -454,18 +466,31 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { TableName.SecretTag, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` - ) - .whereIn("slug", slugs); + ); + + if (slugs?.length) { + void query.whereIn("slug", slugs); + } + } + + if (filters?.includeMetadataInSearch) { + void query.leftJoin( + TableName.ResourceMetadata, + `${TableName.SecretV2}.id`, + `${TableName.ResourceMetadata}.secretId` + ); } const secrets = await query; + // @ts-expect-error not inferred by knex return Number(secrets[0]?.count ?? 0); } catch (error) { throw new DatabaseError({ error, name: "get folder secret count" }); } }; + // This method currently uses too many joins which is not performant, in case we need to add more filters we should consider refactoring this method const findByFolderIds = async (dto: { folderIds: string[]; userId?: string; @@ -485,12 +510,14 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { .whereIn(`${TableName.SecretV2}.folderId`, folderIds) .where((bd) => { if (filters?.search) { + void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); if (filters?.includeTagsInSearch) { + void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); + } + if (filters?.includeMetadataInSearch) { void bd - .whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`) - .orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); - } else { - void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); + .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`) + .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`); } } @@ -513,18 +540,15 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) - .leftJoin( - TableName.SecretReminderRecipients, - `${TableName.SecretV2}.id`, - `${TableName.SecretReminderRecipients}.secretId` - ) - .leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .leftJoin( TableName.SecretRotationV2SecretMapping, `${TableName.SecretV2}.id`, `${TableName.SecretRotationV2SecretMapping}.secretId` ) + .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) + .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) + .leftJoin(TableName.Users, `${TableName.ReminderRecipient}.userId`, `${TableName.Users}.id`) .where((qb) => { if (filters?.metadataFilter && filters.metadataFilter.length > 0) { filters.metadataFilter.forEach((meta) => { @@ -547,7 +571,11 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { }) as rank` ) ) - .select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId")) + .select(db.ref("id").withSchema(TableName.Reminder).as("reminderId")) + .select(db.ref("message").withSchema(TableName.Reminder).as("reminderNote")) + .select(db.ref("repeatDays").withSchema(TableName.Reminder).as("reminderRepeatDays")) + .select(db.ref("nextReminderDate").withSchema(TableName.Reminder).as("nextReminderDate")) + .select(db.ref("id").withSchema(TableName.ReminderRecipient).as("reminderRecipientId")) .select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername")) .select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail")) .select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId")) @@ -809,6 +837,44 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } }; + const findSecretsWithReminderRecipients = async (ids: string[], limit: number, tx?: Knex) => { + try { + // Create a subquery to get limited secret IDs + const limitedSecretIds = (tx || db)(TableName.SecretV2) + .whereIn(`${TableName.SecretV2}.id`, ids) + .limit(limit) + .select("id"); + + // Join with all recipients for the limited secrets + const docs = await (tx || db)(TableName.SecretV2) + .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) + .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) + .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("userId").withSchema(TableName.ReminderRecipient).as("reminderRecipientUserId")); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...SecretsV2Schema.parse(el) + }), + childrenMapper: [ + { + key: "reminderRecipientUserId", + label: "recipients" as const, + mapper: ({ reminderRecipientUserId }) => reminderRecipientUserId + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "FindSecretsWithReminderRecipients" }); + } + }; + return { ...secretOrm, update, @@ -826,6 +892,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { countByFolderIds, findOne, find, - invalidateSecretCacheByProjectId + invalidateSecretCacheByProjectId, + findSecretsWithReminderRecipients }; }; 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 9f62b6d57..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, @@ -225,17 +231,7 @@ export const fnSecretBulkUpdate = async ({ const sanitizedInputSecrets = inputSecrets.map( ({ filter, - data: { - skipMultilineEncoding, - type, - key, - encryptedValue, - userId, - encryptedComment, - metadata, - reminderNote, - reminderRepeatDays - } + data: { skipMultilineEncoding, type, key, encryptedValue, userId, encryptedComment, metadata, secretMetadata } }) => ({ filter: { ...filter, folderId }, data: { @@ -244,10 +240,8 @@ export const fnSecretBulkUpdate = async ({ key, userId, encryptedComment, - metadata, - reminderNote, - encryptedValue, - reminderRepeatDays + metadata: JSON.stringify(metadata || secretMetadata || []), + encryptedValue } }) ); @@ -263,9 +257,7 @@ export const fnSecretBulkUpdate = async ({ encryptedComment, version, metadata, - reminderNote, encryptedValue, - reminderRepeatDays, id: secretId }) => ({ skipMultilineEncoding, @@ -275,9 +267,7 @@ export const fnSecretBulkUpdate = async ({ encryptedComment, version, metadata: metadata ? JSON.stringify(metadata) : [], - reminderNote, encryptedValue, - reminderRepeatDays, folderId, secretId, userActorId, @@ -359,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 })); @@ -395,7 +389,9 @@ export const fnSecretBulkDelete = async ({ secretDAL, secretQueueService, folderCommitService, - secretVersionDAL + secretVersionDAL, + projectId, + commitChanges }: TFnSecretBulkDelete) => { const deletedSecrets = await secretDAL.deleteMany( inputSecrets.map(({ type, secretKey }) => ({ @@ -407,11 +403,14 @@ export const fnSecretBulkDelete = async ({ tx ); - await Promise.allSettled( + await Promise.all( deletedSecrets .filter(({ reminderRepeatDays }) => Boolean(reminderRepeatDays)) .map(({ id, reminderRepeatDays }) => - secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number }, tx) + secretQueueService.removeSecretReminder( + { secretId: id, repeatDays: reminderRepeatDays as number, projectId }, + tx + ) ) ); @@ -421,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 cab41933f..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,10 +35,11 @@ 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"; +import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; import { TGetASecretByIdDTO } from "../secret/secret-types"; @@ -108,6 +116,7 @@ type TSecretV2BridgeServiceFactoryDep = { snapshotService: Pick; resourceMetadataDAL: Pick; keyStore: Pick; + reminderService: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -132,7 +141,8 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalRequestSecretDAL, kmsService, resourceMetadataDAL, - keyStore + keyStore, + reminderService }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, @@ -236,7 +246,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -303,7 +314,6 @@ export const secretV2BridgeServiceFactory = ({ { version: 1, type, - reminderRepeatDays: inputSecretData.secretReminderRepeatDays, encryptedComment: setKnexStringValue( inputSecretData.secretComment, (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob @@ -311,7 +321,6 @@ export const secretV2BridgeServiceFactory = ({ encryptedValue: inputSecretData.secretValue ? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob : undefined, - reminderNote: inputSecretData.secretReminderNote, skipMultilineEncoding: inputSecretData.skipMultilineEncoding, key: secretName, userId: inputSecret.type === SecretType.Personal ? actorId : null, @@ -337,6 +346,20 @@ export const secretV2BridgeServiceFactory = ({ return createdSecret; }); + if (inputSecret.secretReminderRepeatDays) { + await reminderService.createReminder({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + reminder: { + secretId: secret.id, + message: inputSecret.secretReminderNote, + repeatDays: inputSecret.secretReminderRepeatDays + } + }); + } + await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); @@ -379,7 +402,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (inputSecret.newSecretName === "") { @@ -512,12 +536,10 @@ export const secretV2BridgeServiceFactory = ({ { filter: { id: secretId }, data: { - reminderRepeatDays: inputSecret.secretReminderRepeatDays, encryptedComment: setKnexStringValue( inputSecret.secretComment, (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob ), - reminderNote: inputSecret.secretReminderNote, skipMultilineEncoding: inputSecret.skipMultilineEncoding, key: inputSecret.newSecretName || secretName, tags: inputSecret.tagIds, @@ -538,19 +560,20 @@ export const secretV2BridgeServiceFactory = ({ tx }) ); - await secretQueueService.handleSecretReminder({ - newSecret: { - id: updatedSecret[0].id, - ...inputSecret - }, - oldSecret: { - id: secret.id, - secretReminderNote: secret.reminderNote, - secretReminderRepeatDays: secret.reminderRepeatDays, - secretReminderRecipients: secret.secretReminderRecipients?.map((el) => el.user.id) - }, - projectId - }); + if (inputSecret.secretReminderRepeatDays) { + await reminderService.createReminder({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + reminder: { + secretId: secret.id, + message: inputSecret.secretReminderNote, + repeatDays: inputSecret.secretReminderRepeatDays, + recipients: inputSecret.secretReminderRecipients + } + }); + } await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { @@ -606,7 +629,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -742,7 +766,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } @@ -787,7 +812,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -886,7 +912,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -939,7 +966,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); @@ -1237,7 +1265,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId: secret.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { @@ -1300,7 +1329,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -1505,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); @@ -1589,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; @@ -1612,6 +1645,7 @@ export const secretV2BridgeServiceFactory = ({ }; }), folderId, + commitChanges, orgId: actorOrgId, secretDAL, resourceMetadataDAL, @@ -1624,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({ @@ -1672,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); @@ -1702,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" }); @@ -1832,7 +1876,7 @@ export const secretV2BridgeServiceFactory = ({ { operator: "eq", field: `${TableName.SecretV2}.key` as "key", - value: el.secretKey + value: el.newSecretName as string }, { operator: "eq", @@ -1886,6 +1930,7 @@ export const secretV2BridgeServiceFactory = ({ orgId: actorOrgId, folderCommitService, tx, + commitChanges, inputSecrets: secretsToUpdate.map((el) => { const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; const encryptedValue = @@ -1899,12 +1944,10 @@ export const secretV2BridgeServiceFactory = ({ return { filter: { id: originalSecret.id, type: SecretType.Shared }, data: { - reminderRepeatDays: el.secretReminderRepeatDays, encryptedComment: setKnexStringValue( el.secretComment, (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob ), - reminderNote: el.secretReminderNote, skipMultilineEncoding: el.skipMultilineEncoding, key: el.newSecretName || el.secretKey, tags: el.tagIds, @@ -1965,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))); @@ -2022,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); @@ -2082,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); @@ -2185,7 +2242,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId: folder.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const canRead = @@ -2250,7 +2308,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -2297,7 +2356,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); @@ -2553,9 +2613,7 @@ export const secretV2BridgeServiceFactory = ({ key: doc.key, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - reminderNote: doc.reminderNote, secretMetadata: doc.secretMetadata, - reminderRepeatDays: doc.reminderRepeatDays, ...(doc.encryptedValue ? { encryptedValue: doc.encryptedValue, @@ -2683,7 +2741,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { @@ -2776,7 +2835,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, @@ -2900,7 +2960,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const canRead = @@ -2946,6 +3007,11 @@ export const secretV2BridgeServiceFactory = ({ }); }; + const findSecretIdsByFolderIdAndKeys = async ({ folderId, keys }: { folderId: string; keys: string[] }) => { + const secrets = await secretDAL.find({ folderId, $in: { [`${TableName.SecretV2}.key` as "key"]: keys } }); + return secrets.map((el) => ({ id: el.id, key: el.key })); + }; + return { createSecret, deleteSecret, @@ -2965,6 +3031,7 @@ export const secretV2BridgeServiceFactory = ({ getSecretsByFolderMappings, getSecretById, getAccessibleSecrets, - getSecretVersionsByIds + getSecretVersionsByIds, + findSecretIdsByFolderIdAndKeys }; }; 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 f4171b1a7..fc7d468ff 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; @@ -246,6 +249,7 @@ export type TCreateSecretReminderDTO = { export type TRemoveSecretReminderDTO = { secretId: string; repeatDays: number; + projectId: string; }; export type TBackFillSecretReferencesDTO = TProjectPermission; @@ -355,6 +359,7 @@ export type TFindSecretsByFolderIdsFilter = { tagSlugs?: string[]; metadataFilter?: { key?: string; value?: string }[]; includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; keys?: string[]; }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index dc41d129f..a540d3435 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -5,8 +5,6 @@ import { TDbClient } from "@app/db"; import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; -import { logger } from "@app/lib/logger"; -import { QueueName, TQueueServiceFactory } from "@app/queue"; export type TSecretDALFactory = ReturnType; @@ -383,94 +381,6 @@ export const secretDALFactory = (db: TDbClient) => { } }; - const pruneSecretReminders = async (queueService: TQueueServiceFactory) => { - const REMINDER_PRUNE_BATCH_SIZE = 5_000; - const MAX_RETRY_ON_FAILURE = 3; - let numberOfRetryOnFailure = 0; - let deletedReminderCount = 0; - - logger.info(`${QueueName.DailyResourceCleanUp}: secret reminders started`); - - try { - const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder); - const reminderJobs = repeatableJobs - .map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key })) - .filter(Boolean); - - if (reminderJobs.length === 0) { - logger.info(`${QueueName.DailyResourceCleanUp}: no reminder jobs found`); - return; - } - - for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) { - try { - const batchIds = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE).map((r) => r.secretId); - - const payload = { - $in: { - id: batchIds - } - }; - - const opts = { - limit: REMINDER_PRUNE_BATCH_SIZE - }; - - // Find existing secrets with pagination - // eslint-disable-next-line no-await-in-loop - const [secrets, secretsV2] = await Promise.all([ - ormify(db, TableName.Secret).find(payload, opts), - ormify(db, TableName.SecretV2).find(payload, opts) - ]); - - const foundSecretIds = new Set([ - ...secrets.map((secret) => secret.id), - ...secretsV2.map((secret) => secret.id) - ]); - - // Find IDs that don't exist in either table - const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId)); - - // Delete reminders for non-existent secrets - for (const secretId of secretIdsNotFound) { - const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey; - - if (jobKey) { - // eslint-disable-next-line no-await-in-loop - await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey); - deletedReminderCount += 1; - } - } - - numberOfRetryOnFailure = 0; - } catch (error) { - numberOfRetryOnFailure += 1; - logger.error(error, `Failed to process batch at offset ${offset}`); - - if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) { - break; - } - - // Retry the current batch - offset -= REMINDER_PRUNE_BATCH_SIZE; - - // eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop - await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure)); - } - - // Small delay between batches - // eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } catch (error) { - logger.error(error, "Failed to complete secret reminder pruning"); - } finally { - logger.info( - `${QueueName.DailyResourceCleanUp}: secret reminders completed. Deleted ${deletedReminderCount} reminders` - ); - } - }; - return { ...secretOrm, update, @@ -485,7 +395,6 @@ export const secretDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - pruneSecretReminders, findManySecretsWithTags }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 15a31a331..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, @@ -17,11 +18,9 @@ import { ProjectPermissionSecretActions } from "@app/ee/services/permission/proj import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName } from "@app/lib/crypto"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; -import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; -import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, @@ -33,6 +32,7 @@ import { KmsDataKey } from "../kms/kms-types"; import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretDALFactory } from "./secret-dal"; @@ -181,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 @@ -743,7 +744,8 @@ export const fnSecretBulkDelete = async ({ tx, actorId, secretDAL, - secretQueueService + secretQueueService, + projectId }: TFnSecretBulkDelete) => { const deletedSecrets = await secretDAL.deleteMany( inputSecrets.map(({ type, secretBlindIndex }) => ({ @@ -759,7 +761,10 @@ export const fnSecretBulkDelete = async ({ deletedSecrets .filter(({ secretReminderRepeatDays }) => Boolean(secretReminderRepeatDays)) .map(({ id, secretReminderRepeatDays }) => - secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number }, tx) + secretQueueService.removeSecretReminder( + { secretId: id, repeatDays: secretReminderRepeatDays as number, projectId }, + tx + ) ) ); @@ -1226,14 +1231,14 @@ export const decryptSecretWithBot = ( type TFnDeleteProjectSecretReminders = { secretDAL: Pick; secretV2BridgeDAL: Pick; - queueService: Pick; + reminderService: Pick; projectBotService: Pick; folderDAL: Pick; }; export const fnDeleteProjectSecretReminders = async ( projectId: string, - { secretDAL, secretV2BridgeDAL, queueService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders + { secretDAL, secretV2BridgeDAL, reminderService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders ) => { const projectFolders = await folderDAL.findByProjectId(projectId); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId, false); @@ -1248,23 +1253,13 @@ export const fnDeleteProjectSecretReminders = async ( $notNull: ["secretReminderRepeatDays"] }); - const appCfg = getConfig(); for await (const secret of projectSecrets) { const repeatDays = shouldUseSecretV2Bridge ? (secret as { reminderRepeatDays: number }).reminderRepeatDays : (secret as { secretReminderRepeatDays: number }).secretReminderRepeatDays; - // We're using the queue service directly to get around conflicting imports. if (repeatDays) { - await queueService.stopRepeatableJob( - QueueName.SecretReminder, - QueueJobs.SecretReminder, - { - // on prod it this will be in days, in development this will be second - every: appCfg.NODE_ENV === "development" ? secondsToMillis(repeatDays) : daysToMillisecond(repeatDays) - }, - `reminder-${secret.id}` - ); + await reminderService.deleteReminderBySecretId(secret.id, projectId); } } }; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index b178aa8e1..689e19673 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -19,7 +19,6 @@ import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sn import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; -import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -40,7 +39,6 @@ import { TIntegrationAuthServiceFactory } from "../integration-auth/integration- import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; -import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectDALFactory } from "../project/project-dal"; import { createProjectKey } from "../project/project-fns"; @@ -49,12 +47,12 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; -import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; @@ -92,7 +90,6 @@ type TSecretQueueFactoryDep = { projectKeyDAL: Pick; projectMembershipDAL: Pick; smtpService: TSmtpService; - orgDAL: Pick; secretVersionDAL: TSecretVersionDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretTagDAL: TSecretTagDALFactory; @@ -112,11 +109,8 @@ type TSecretQueueFactoryDep = { projectUserMembershipRoleDAL: Pick; resourceMetadataDAL: Pick; folderCommitService: Pick; - secretReminderRecipientsDAL: Pick< - TSecretReminderRecipientsDALFactory, - "delete" | "findUsersBySecretId" | "insertMany" | "transaction" - >; secretSyncQueue: Pick; + reminderService: Pick; }; export type TGetSecrets = { @@ -154,7 +148,6 @@ export const secretQueueFactory = ({ userDAL, webhookDAL, projectEnvDAL, - orgDAL, smtpService, projectDAL, projectBotDAL, @@ -177,9 +170,9 @@ export const secretQueueFactory = ({ projectUserMembershipRoleDAL, projectKeyDAL, resourceMetadataDAL, - secretReminderRecipientsDAL, secretSyncQueue, - folderCommitService + folderCommitService, + reminderService }: TSecretQueueFactoryDep) => { const integrationMeter = opentelemetry.metrics.getMeter("Integrations"); const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", { @@ -189,19 +182,8 @@ export const secretQueueFactory = ({ const removeSecretReminder = async ({ deleteRecipients = true, ...dto }: TRemoveSecretReminderDTO, tx?: Knex) => { if (deleteRecipients) { - await secretReminderRecipientsDAL.delete({ secretId: dto.secretId }, tx); + await reminderService.deleteReminderBySecretId(dto.secretId, dto.projectId, tx); } - - const appCfg = getConfig(); - await queueService.stopRepeatableJob( - QueueName.SecretReminder, - QueueJobs.SecretReminder, - { - // on prod it this will be in days, in development this will be second - every: appCfg.NODE_ENV === "development" ? secondsToMillis(dto.repeatDays) : daysToMillisecond(dto.repeatDays) - }, - `reminder-${dto.secretId}` - ); }; const $generateActor = async (actorId?: string, isManual?: boolean): Promise => { @@ -241,11 +223,9 @@ export const secretQueueFactory = ({ oldSecret, newSecret, projectId, - deleteRecipients = true + secretReminderRecipients }: TCreateSecretReminderDTO) => { try { - const appCfg = getConfig(); - if (oldSecret.id !== newSecret.id) { throw new BadRequestError({ name: "SecretReminderIdMismatch", @@ -260,38 +240,13 @@ export const secretQueueFactory = ({ }); } - // If the secret already has a reminder, we should remove the existing one first. - if (oldSecret.secretReminderRepeatDays) { - await removeSecretReminder({ - repeatDays: oldSecret.secretReminderRepeatDays, - secretId: oldSecret.id, - deleteRecipients - }); - } - - await queueService.queue( - QueueName.SecretReminder, - QueueJobs.SecretReminder, - { - note: newSecret.secretReminderNote, - projectId, - repeatDays: newSecret.secretReminderRepeatDays, - secretId: newSecret.id - }, - { - jobId: `reminder-${newSecret.id}`, - repeat: { - // on prod it this will be in days, in development this will be second - every: - appCfg.NODE_ENV === "development" - ? secondsToMillis(newSecret.secretReminderRepeatDays) - : daysToMillisecond(newSecret.secretReminderRepeatDays), - immediately: true - }, - removeOnComplete: true, - removeOnFail: true - } - ); + await reminderService.createReminderInternal({ + secretId: newSecret.id, + message: newSecret.secretReminderNote, + repeatDays: newSecret.secretReminderRepeatDays, + recipients: secretReminderRecipients, + projectId + }); } catch (err) { logger.error(err, "Failed to create secret reminder."); throw new BadRequestError({ @@ -304,55 +259,30 @@ export const secretQueueFactory = ({ const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => { const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret; - const recipientsUpdated = - secretReminderRecipients?.some( - (newId) => !oldSecret.secretReminderRecipients?.find((oldId) => newId === oldId) - ) || secretReminderRecipients?.length !== oldSecret.secretReminderRecipients?.length; - - await secretReminderRecipientsDAL.transaction(async (tx) => { - if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) { - if ( - (secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) || - (secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote) - ) { - await addSecretReminder({ - oldSecret, - newSecret, - projectId, - deleteRecipients: false - }); - } else if ( - secretReminderRepeatDays === null && - secretReminderNote === null && - oldSecret.secretReminderRepeatDays - ) { - await removeSecretReminder({ - secretId: oldSecret.id, - repeatDays: oldSecret.secretReminderRepeatDays - }); - } + if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) { + if ( + (secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) || + (secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote) + ) { + await addSecretReminder({ + oldSecret, + newSecret, + projectId, + secretReminderRecipients: secretReminderRecipients ?? [], + deleteRecipients: false + }); + } else if ( + secretReminderRepeatDays === null && + secretReminderNote === null && + oldSecret.secretReminderRepeatDays + ) { + await removeSecretReminder({ + secretId: oldSecret.id, + repeatDays: oldSecret.secretReminderRepeatDays, + projectId + }); } - - if (recipientsUpdated) { - // if no recipients, delete all existing recipients - if (!secretReminderRecipients?.length) { - const existingRecipients = await secretReminderRecipientsDAL.findUsersBySecretId(newSecret.id, tx); - if (existingRecipients) { - await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx); - } - } else { - await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx); - await secretReminderRecipientsDAL.insertMany( - secretReminderRecipients.map((r) => ({ - secretId: newSecret.id, - userId: r, - projectId - })), - tx - ); - } - } - }); + } }; const createManySecretsRawFn = createManySecretsRawFnFactory({ projectDAL, @@ -747,7 +677,7 @@ export const secretQueueFactory = ({ environment: jobPayload.environmentName, count: jobPayload.count, projectName: project.name, - integrationUrl: `${appCfg.SITE_URL}/projects/${project.id}/secret-manager/integrations?selectedTab=native-integrations` + integrationUrl: `${appCfg.SITE_URL}/projects/secret-management/${project.id}/integrations?selectedTab=native-integrations` } }); } @@ -1118,62 +1048,9 @@ export const secretQueueFactory = ({ } }); + // TODO(Carlos): remove this queue (needed for queue initialization and perform the migration) queueService.start(QueueName.SecretReminder, async ({ data }) => { - logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}]`); - - const { projectId } = data; - - const organization = await orgDAL.findOrgByProjectId(projectId); - const project = await projectDAL.findById(projectId); - const secret = await secretV2BridgeDAL.findById(data.secretId); - const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]); - - const recipients = await secretReminderRecipientsDAL.findUsersBySecretId(data.secretId); - - if (!organization) { - logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`); - return; - } - - if (!project) { - logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no project found`); - return; - } - - const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); - - if (!projectMembers || !projectMembers.length) { - logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no project members found`); - return; - } - - const selectedRecipients = recipients?.length - ? recipients.map((r) => r.email as string) - : projectMembers.map((m) => m.user.email as string); - - await smtpService.sendMail({ - template: SmtpTemplates.SecretReminder, - subjectLine: "Infisical secret reminder", - recipients: selectedRecipients, - substitutions: { - reminderNote: data.note, // May not be present. - projectName: project.name, - organizationName: organization.name - } - }); - - await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, { - type: WebhookEvents.SecretReminderExpired, - payload: { - projectName: project.name, - projectId: project.id, - secretPath: folder?.path, - environment: folder?.environmentSlug || "", - reminderNote: data.note, - secretName: secret?.key, - secretId: data.secretId - } - }); + logger.info(`(deprecated) secretReminderQueue.process: [secretDocument=${data.secretId}]`); }); const startSecretV2Migration = async (projectId: string) => { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 084423fa9..f776b4527 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, @@ -45,6 +46,7 @@ import { ChangeType } from "../folder-commit/folder-commit-service"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; @@ -128,6 +130,7 @@ type TSecretServiceFactoryDep = { "insertMany" | "insertApprovalSecretTags" >; licenseService: Pick; + reminderService: Pick; }; export type TSecretServiceFactory = ReturnType; @@ -150,7 +153,8 @@ export const secretServiceFactory = ({ secretApprovalRequestSecretDAL, secretV2BridgeService, secretApprovalRequestService, - licenseService + licenseService, + reminderService }: TSecretServiceFactoryDep) => { const getSecretReference = async (projectId: string) => { // if bot key missing means e2e still exist @@ -212,7 +216,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -329,7 +334,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -489,7 +495,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -547,7 +554,8 @@ export const secretServiceFactory = ({ await secretQueueService.removeSecretReminder( { repeatDays: secret.secretReminderRepeatDays, - secretId: secret.id + secretId: secret.id, + projectId }, tx ); @@ -604,7 +612,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); let paths: { folderId: string; path: string }[] = []; @@ -709,7 +718,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, @@ -814,7 +824,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.Create, @@ -900,7 +911,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -1022,7 +1034,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.Delete, @@ -1073,7 +1086,8 @@ export const secretServiceFactory = ({ await secretQueueService.removeSecretReminder( { repeatDays: secret.secretReminderRepeatDays, - secretId: secret.id + secretId: secret.id, + projectId }, tx ); @@ -1128,6 +1142,8 @@ export const secretServiceFactory = ({ | "environment" | "tagSlugs" | "search" + | "includeTagsInSearch" + | "includeMetadataInSearch" >) => { const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1661,8 +1677,6 @@ export const secretServiceFactory = ({ secretComment, secretValue, tagIds, - reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays, secretMetadata } ] @@ -1844,9 +1858,6 @@ export const secretServiceFactory = ({ secretComment, secretValue, tagIds, - reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays, - secretReminderRecipients, secretMetadata } ] @@ -1855,9 +1866,6 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Approval as const, approval }; } const secret = await secretV2BridgeService.updateSecret({ - secretReminderRepeatDays, - secretReminderNote, - secretReminderRecipients, skipMultilineEncoding, tagIds, secretComment, @@ -1875,6 +1883,21 @@ export const secretServiceFactory = ({ secretValue, secretMetadata }); + + if (secretReminderRepeatDays) { + await reminderService.createReminder({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + reminder: { + secretId: secret.id, + message: secretReminderNote, + repeatDays: secretReminderRepeatDays, + recipients: secretReminderRecipients + } + }); + } return { type: SecretProtectionType.Direct as const, secret }; } @@ -2307,6 +2330,29 @@ export const secretServiceFactory = ({ secrets: inputSecrets, mode }); + + await Promise.all( + inputSecrets + .filter((el) => el.secretReminderRepeatDays) + .map(async (secret) => { + await reminderService.createReminder({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + reminder: { + secretId: secrets.find( + (el) => + (el.secretKey === secret.secretKey || el.secretKey === secret.newSecretName) && + el.secretPath === (secret.secretPath || secretPath) + )?.id, + message: secret.secretReminderNote, + repeatDays: secret.secretReminderRepeatDays + } + }); + }) + ); + return { type: SecretProtectionType.Direct as const, secrets }; } @@ -2551,7 +2597,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, { @@ -2643,7 +2690,8 @@ export const secretServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -2748,7 +2796,8 @@ export const secretServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -2854,7 +2903,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -2939,7 +2989,8 @@ export const secretServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const { botKey } = await projectBotService.getBotKey(project.id); @@ -3346,7 +3397,8 @@ export const secretServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -3374,7 +3426,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 12b8e7175..cd341a8b5 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -212,6 +212,8 @@ export type TGetSecretsRawDTO = { limit?: number; search?: string; keys?: string[]; + includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; } & TProjectPermission; export type TGetSecretAccessListDTO = { @@ -310,6 +312,7 @@ export type TUpdateManySecretRawDTO = Omit & { secretMetadata?: ResourceMetadataDTO; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretPath?: string; }[]; }; @@ -410,6 +413,7 @@ export type TCreateSecretReminderDTO = { oldSecret: TPartialSecret; newSecret: TPartialSecret; projectId: string; + secretReminderRecipients: string[]; deleteRecipients?: boolean; }; @@ -417,6 +421,7 @@ export type TCreateSecretReminderDTO = { export type TRemoveSecretReminderDTO = { secretId: string; repeatDays: number; + projectId: string; deleteRecipients?: boolean; }; @@ -544,3 +549,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..2aa495673 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); @@ -210,7 +214,7 @@ export const serviceTokenServiceFactory = ({ substitutions: { tokenName: token.name, projectName: token.projectName, - url: `${appCfg.SITE_URL}/projects/${token.projectId}/secret-manager/access-management?selectedTab=service-tokens` + url: `${appCfg.SITE_URL}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens` } }); await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); 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/digital-ocean/available.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/available.mdx new file mode 100644 index 000000000..78799ff30 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/digital-ocean/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/create.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/create.mdx new file mode 100644 index 000000000..dc526fef7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/digital-ocean" +--- + + + Check out the configuration docs for [Digital Ocean Connections](/integrations/app-connections/digital-ocean) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/delete.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/delete.mdx new file mode 100644 index 000000000..0ec3daf3d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/digital-ocean/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-id.mdx new file mode 100644 index 000000000..522a88efc --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/digital-ocean/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-name.mdx new file mode 100644 index 000000000..e950b6ef8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/digital-ocean/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/list.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/list.mdx new file mode 100644 index 000000000..d49d1621a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/digital-ocean" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/update.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/update.mdx new file mode 100644 index 000000000..5ba755d67 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/digital-ocean/{connectionId}" +--- + + + Check out the configuration docs for [Digital Ocean Connections](/integrations/app-connections/digital-ocean) to learn how to obtain the required credentials. + 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/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create.mdx new file mode 100644 index 000000000..673fe7c0a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/digital-ocean-app-platform" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete.mdx new file mode 100644 index 000000000..79af0d463 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id.mdx new file mode 100644 index 000000000..d8b9e9b68 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name.mdx new file mode 100644 index 000000000..82cf109e2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/digital-ocean-app-platform/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list.mdx new file mode 100644 index 000000000..e7c779906 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/digital-ocean-app-platform" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets.mdx new file mode 100644 index 000000000..eee30d627 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets.mdx new file mode 100644 index 000000000..4d3e66c13 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update.mdx new file mode 100644 index 000000000..c6c8d45e1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index a32453c89..13ec5367e 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" @@ -469,6 +476,7 @@ "integrations/app-connections/checkly", "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", + "integrations/app-connections/digital-ocean", "integrations/app-connections/flyio", "integrations/app-connections/gcp", "integrations/app-connections/github", @@ -481,6 +489,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,11 +517,13 @@ "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", "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", + "integrations/secret-syncs/digital-ocean-app-platform", "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", @@ -551,7 +562,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 +677,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 +1005,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 +1113,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": [ @@ -1351,6 +1384,18 @@ "api-reference/endpoints/app-connections/databricks/update", "api-reference/endpoints/app-connections/databricks/delete" ] + }, + { + "group": "Digital Ocean", + "pages": [ + "api-reference/endpoints/app-connections/digital-ocean/list", + "api-reference/endpoints/app-connections/digital-ocean/available", + "api-reference/endpoints/app-connections/digital-ocean/get-by-id", + "api-reference/endpoints/app-connections/digital-ocean/get-by-name", + "api-reference/endpoints/app-connections/digital-ocean/create", + "api-reference/endpoints/app-connections/digital-ocean/update", + "api-reference/endpoints/app-connections/digital-ocean/delete" + ] }, { "group": "Fly.io", @@ -1496,6 +1541,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 +1764,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": [ @@ -1772,6 +1843,19 @@ "api-reference/endpoints/secret-syncs/databricks/remove-secrets" ] }, + { + "group": "Digital Ocean", + "pages": [ + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets" + ] + }, { "group": "Fly.io", "pages": [ @@ -2228,6 +2312,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/digital-ocean/app-connection-create-api-key.png b/docs/images/app-connections/digital-ocean/app-connection-create-api-key.png new file mode 100644 index 000000000..8b1bc20da Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-api-key.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-form-roles.png b/docs/images/app-connections/digital-ocean/app-connection-create-form-roles.png new file mode 100644 index 000000000..43033cf12 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-form-roles.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-form.png b/docs/images/app-connections/digital-ocean/app-connection-create-form.png new file mode 100644 index 000000000..32fb7f9e4 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-form.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-required-roles.png b/docs/images/app-connections/digital-ocean/app-connection-create-required-roles.png new file mode 100644 index 000000000..1fe865fb4 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-required-roles.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-form.png b/docs/images/app-connections/digital-ocean/app-connection-form.png new file mode 100644 index 000000000..b96fb34d7 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-form.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-generated.png b/docs/images/app-connections/digital-ocean/app-connection-generated.png new file mode 100644 index 000000000..0bda0e435 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-generated.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-key-generated.png b/docs/images/app-connections/digital-ocean/app-connection-key-generated.png new file mode 100644 index 000000000..4ee42881e Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-key-generated.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-option.png b/docs/images/app-connections/digital-ocean/app-connection-option.png new file mode 100644 index 000000000..38336dc32 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-option.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-profile.png b/docs/images/app-connections/digital-ocean/app-connection-profile.png new file mode 100644 index 000000000..a9cb9a1df Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-profile.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/images/secret-syncs/digital-ocean-app-platform/select-option.png b/docs/images/secret-syncs/digital-ocean-app-platform/select-option.png new file mode 100644 index 000000000..f1709364f Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/select-option.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-created.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-created.png new file mode 100644 index 000000000..4359de5f8 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-created.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-destination.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-destination.png new file mode 100644 index 000000000..278cc7946 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-destination.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-details.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-details.png new file mode 100644 index 000000000..d3d4d9c33 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-details.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-options.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-options.png new file mode 100644 index 000000000..bd3137dbe Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-options.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-review.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-review.png new file mode 100644 index 000000000..e0e5f10bf Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-review.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-source.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-source.png new file mode 100644 index 000000000..1ce54d152 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-source.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/digital-ocean.mdx b/docs/integrations/app-connections/digital-ocean.mdx new file mode 100644 index 000000000..5b047f017 --- /dev/null +++ b/docs/integrations/app-connections/digital-ocean.mdx @@ -0,0 +1,113 @@ +--- +title: "DigitalOcean Connection" +description: "Learn how to configure a DigitalOcean Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/account/api/tokens) to connect with DigitalOcean. + +## Create a DigitalOcean API Token + + + + ![DigitalOcean Dashboard](/images/app-connections/digital-ocean/app-connection-profile.png) + + + ![API Section](/images/app-connections/digital-ocean/app-connection-create-api-key.png) + + + Give your token a descriptive name and ensure custom scopes is selected. + + ![Token Form](/images/app-connections/digital-ocean/app-connection-create-form.png) + + + ``` + read:account + read:actions + read:regions + read:sizes + read:app/projects + update:app + ``` + + ![Token Form](/images/app-connections/digital-ocean/app-connection-create-form-roles.png) + ![Token Form](/images/app-connections/digital-ocean/app-connection-create-required-roles.png) + + + Make sure to copy the token now—you won't be able to see it again. + + ![Token Generated](/images/app-connections/digital-ocean/app-connection-key-generated.png) + + + +## Create a DigitalOcean Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **+ Add Connection** and choose **DigitalOcean Connection** from the list of integrations. + + ![Select DigitalOcean Connection](/images/app-connections/digital-ocean/app-connection-option.png) + + + Complete the form by providing: + - A descriptive name for the connection + - An optional description + - The API Token from the previous step + + ![DigitalOcean Connection Modal](/images/app-connections/digital-ocean/app-connection-form.png) + + + After submitting the form, your **DigitalOcean Connection** will be successfully created and ready to use with your Infisical projects. + + ![DigitalOcean Connection Created](/images/app-connections/digital-ocean/app-connection-generated.png) + + + + + + To create a DigitalOcean Connection via API, send a request to the [Create DigitalOcean Connection](/api-reference/endpoints/app-connections/digital-ocean/create) endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/digital-ocean \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-digitalocean-connection", + "method": "api-token", + "credentials": { + "apiToken": "[API TOKEN]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", + "name": "my-digitalocean-connection", + "description": null, + "version": 1, + "orgId": "abcdef12-3456-7890-abcd-ef1234567890", + "createdAt": "2025-07-19T10:15:00.000Z", + "updatedAt": "2025-07-19T10:15:00.000Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e", + "app": "digital-ocean", + "method": "api-token", + "credentials": {} + } + } + ``` + + + 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/integrations/secret-syncs/digital-ocean-app-platform.mdx b/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx new file mode 100644 index 000000000..91657c760 --- /dev/null +++ b/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx @@ -0,0 +1,155 @@ +--- +title: "DigitalOcean App Platform Sync" +description: "Learn how to configure a DigitalOcean App Platform Sync for Infisical." +--- + +**Prerequisites:** + +- Create a [DigitalOcean Connection](/integrations/app-connections/digital-ocean) + + + + + + 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 DigitalOcean](/images/secret-syncs/digital-ocean-app-platform/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/digital-ocean-app-platform/sync-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/digital-ocean-app-platform/sync-destination.png) + + - **DigitalOcean Connection**: The DigitalOcean Connection to authenticate with. + - **App**: The App Platform app to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/digital-ocean-app-platform/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. + + Digital Ocean App Platform 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 DigitalOcean Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/digital-ocean-app-platform/sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your DigitalOcean Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/digital-ocean-app-platform/sync-review.png) + + + If enabled, your DigitalOcean Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/digital-ocean-app-platform/sync-created.png) + + + + + + To create a **DigitalOcean App Platform Sync**, make an API request to the [Create DigitalOcean Sync](/api-reference/endpoints/secret-syncs/digital-ocean/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/digital-ocean-app-platform \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-digitalocean-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "sync to do app", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/app-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "destinationConfig": { + "appId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "appName": "do-todo-app" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-digitalocean-sync", + "description": "sync to do app", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-07-19T12:00:00Z", + "updatedAt": "2025-07-19T12:00:00Z", + "syncStatus": "succeeded", + "lastSyncJobId": "job-5678", + "lastSyncMessage": null, + "lastSyncedAt": "2025-07-19T12:00:00Z", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "digital-ocean", + "name": "my-digitalocean-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": "/app-secrets" + }, + "destination": "digital-ocean", + "destinationConfig": { + "appId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "appName": "do-todo-app" + } + } + } + ``` + + 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: "Certificates 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/DigitalOceanAppPlatformSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx new file mode 100644 index 000000000..e50c82c3f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx @@ -0,0 +1,63 @@ +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 { useDigitalOceanConnectionListApps } from "@app/hooks/api/appConnections/digital-ocean"; +import { TDigitalOceanApp } from "@app/hooks/api/appConnections/digital-ocean/types"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const DigitalOceanAppPlatformSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.DigitalOceanAppPlatform } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: apps = [], isPending: isAccountsLoading } = useDigitalOceanConnectionListApps( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + return ( + <> + { + setValue("destinationConfig.appId", ""); + setValue("destinationConfig.appName", ""); + }} + /> + ( + + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.appName", v?.spec.name ?? ""); + }} + options={apps} + placeholder="Select an app..." + getOptionLabel={(option) => option.spec.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 09cf2caec..478447541 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -9,11 +9,13 @@ 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"; import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; +import { DigitalOceanAppPlatformSyncFields } from "./DigitalOceanAppPlatformSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; @@ -91,6 +93,10 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Supabase: return ; + case SecretSync.DigitalOceanAppPlatform: + 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..50ea46ac0 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -63,6 +63,8 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Railway: case SecretSync.Checkly: case SecretSync.Supabase: + case SecretSync.DigitalOceanAppPlatform: + 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/DigitalOceanAppPlatformSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DigitalOceanAppPlatformSyncReviewFields.tsx new file mode 100644 index 000000000..e99d7c623 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DigitalOceanAppPlatformSyncReviewFields.tsx @@ -0,0 +1,14 @@ +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 DigitalOceanAppPlatformSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.DigitalOceanAppPlatform } + >(); + const appName = watch("destinationConfig.appName"); + + return {appName}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index d09f58a2e..c1194a4c1 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -18,11 +18,13 @@ 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"; import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; +import { DigitalOceanAppPlatformSyncReviewFields } from "./DigitalOceanAppPlatformSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; @@ -144,6 +146,12 @@ export const SecretSyncReviewFields = () => { case SecretSync.Supabase: DestinationFieldsComponent = ; break; + case SecretSync.DigitalOceanAppPlatform: + 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/digital-ocean-app-platform-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts new file mode 100644 index 000000000..2840f8b27 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts @@ -0,0 +1,20 @@ +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 DigitalOceanAppPlatformSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.DigitalOceanAppPlatform), + destinationConfig: z.object({ + appId: z + .string() + .min(1, "App ID is required") + .max(255, "App ID must be less than 255 characters"), + appName: z + .string() + .min(1, "Account Name is required") + .max(255, "App Name must be less than 255 characters") + }) + }) +); 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..5ffd5940a 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,11 +6,13 @@ 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"; import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; +import { DigitalOceanAppPlatformSyncDestinationSchema } from "./digital-ocean-app-platform-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; @@ -55,7 +57,9 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ SupabaseSyncDestinationSchema, ZabbixSyncDestinationSchema, RailwaySyncDestinationSchema, - ChecklySyncDestinationSchema + ChecklySyncDestinationSchema, + DigitalOceanAppPlatformSyncDestinationSchema, + 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 88520f31a..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"; @@ -18,6 +20,33 @@ export type DatePickerProps = Omit & { popUpProps: PopoverProps; 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/ @@ -27,12 +56,17 @@ export const DatePicker = ({ popUpProps, 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) { @@ -57,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", @@ -85,14 +126,16 @@ export const DatePicker = ({ }} />
-
- -
+ {!hideTime && ( +
+ +
+ )} ); 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..1345cb493 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -30,6 +30,7 @@ import { LdapConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, + OktaConnectionMethod, OnePassConnectionMethod, OracleDBConnectionMethod, PostgresConnectionMethod, @@ -42,6 +43,7 @@ import { } 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 { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean"; 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"; @@ -98,7 +100,12 @@ 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.DigitalOcean]: { + name: "Digital Ocean", + image: "Digital Ocean.png" + }, + [AppConnection.Okta]: { name: "Okta", image: "Okta.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -132,6 +139,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case CloudflareConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case DigitalOceanConnectionMethod.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..0eb41e119 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -101,6 +101,14 @@ 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.DigitalOceanAppPlatform]: AppConnection.DigitalOcean, + [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/digital-ocean/index.ts b/frontend/src/hooks/api/appConnections/digital-ocean/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/digital-ocean/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/appConnections/digital-ocean/queries.ts b/frontend/src/hooks/api/appConnections/digital-ocean/queries.ts new file mode 100644 index 000000000..7b75fb819 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/digital-ocean/queries.ts @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TDigitalOceanApp } from "./types"; + +const digitalOceanAppPlatformConnectionKeys = { + all: [...appConnectionKeys.all, "digitalOceanAppPlatform"] as const, + listAccounts: (connectionId: string) => + [...digitalOceanAppPlatformConnectionKeys.all, "workspace-scopes", connectionId] as const +}; + +export const useDigitalOceanConnectionListApps = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TDigitalOceanApp[], + unknown, + TDigitalOceanApp[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: digitalOceanAppPlatformConnectionKeys.listAccounts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ apps: TDigitalOceanApp[] }>( + `/api/v1/app-connections/digital-ocean/${connectionId}/apps` + ); + + return data.apps; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/digital-ocean/types.ts b/frontend/src/hooks/api/appConnections/digital-ocean/types.ts new file mode 100644 index 000000000..065730dfa --- /dev/null +++ b/frontend/src/hooks/api/appConnections/digital-ocean/types.ts @@ -0,0 +1,16 @@ +export type TDigitalOceanAppPlatformVariable = { + key: string; + value: string; + type: "SECRET" | "GENERAL"; +}; + +export type TDigitalOceanApp = { + id: string; + spec: { + name: string; + services: Array<{ + name: string; + }>; + envs?: TDigitalOceanAppPlatformVariable[]; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 965675bc7..c3a1fec05 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -32,5 +32,7 @@ export enum AppConnection { Zabbix = "zabbix", Railway = "railway", Checkly = "checkly", - Supabase = "supabase" + Supabase = "supabase", + DigitalOcean = "digital-ocean", + 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..7dc5907e3 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -152,6 +152,14 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Supabase; }; +export type TDigitalOceanConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.DigitalOcean; +}; + +export type TOktaConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Okta; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -183,7 +191,9 @@ export type TAppConnectionOption = | TBitbucketConnectionOption | TZabbixConnectionOption | TRailwayConnectionOption - | TChecklyConnectionOption; + | TChecklyConnectionOption + | TDigitalOceanConnectionOption + | TOktaConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -220,4 +230,6 @@ export type TAppConnectionOptionMap = { [AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Checkly]: TChecklyConnectionOption; [AppConnection.Supabase]: TSupabaseConnectionOption; + [AppConnection.DigitalOcean]: TDigitalOceanConnectionOption; + [AppConnection.Okta]: TOktaConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/digital-ocean.ts b/frontend/src/hooks/api/appConnections/types/digital-ocean.ts new file mode 100644 index 000000000..52b7dcfe9 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/digital-ocean.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 DigitalOceanConnectionMethod { + ApiToken = "api-token" +} + +export type TDigitalOceanConnection = TRootAppConnection & { + app: AppConnection.DigitalOcean; + method: DigitalOceanConnectionMethod.ApiToken; + credentials: { + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 3fc659793..2924c3009 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -12,6 +12,7 @@ import { TCamundaConnection } from "./camunda-connection"; import { TChecklyConnection } from "./checkly-connection"; import { TCloudflareConnection } from "./cloudflare-connection"; import { TDatabricksConnection } from "./databricks-connection"; +import { TDigitalOceanConnection } from "./digital-ocean"; import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; @@ -24,6 +25,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 +46,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 +61,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 +107,9 @@ export type TAppConnection = | TZabbixConnection | TRailwayConnection | TChecklyConnection - | TSupabaseConnection; + | TSupabaseConnection + | TDigitalOceanConnection + | TOktaConnection; export type TAvailableAppConnection = Pick; @@ -172,4 +180,6 @@ export type TAppConnectionMap = { [AppConnection.Railway]: TRailwayConnection; [AppConnection.Checkly]: TChecklyConnection; [AppConnection.Supabase]: TSupabaseConnection; + [AppConnection.DigitalOcean]: TDigitalOceanConnection; + [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/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/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/reminders/index.tsx b/frontend/src/hooks/api/reminders/index.tsx new file mode 100644 index 000000000..b5f16bd3f --- /dev/null +++ b/frontend/src/hooks/api/reminders/index.tsx @@ -0,0 +1 @@ +export { useCreateReminder, useDeleteReminder, useGetReminder } from "./queries"; diff --git a/frontend/src/hooks/api/reminders/queries.tsx b/frontend/src/hooks/api/reminders/queries.tsx new file mode 100644 index 000000000..68c00913d --- /dev/null +++ b/frontend/src/hooks/api/reminders/queries.tsx @@ -0,0 +1,60 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { CreateReminderDTO, DeleteReminderDTO, Reminder } from "./types"; + +export const reminderKeys = { + getReminder: (secretId: string) => ["get-reminder", secretId] as const +}; + +export const useCreateReminder = (secretId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ message, repeatDays, nextReminderDate, recipients }) => { + const { data } = await apiRequest.post<{ reminder: Reminder }>( + `/api/v1/reminders/secrets/${secretId}`, + { + message, + repeatDays, + nextReminderDate, + recipients + } + ); + return data.reminder; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) }); + } + }); +}; + +export const useDeleteReminder = (secretId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { + const { data } = await apiRequest.delete<{ reminder: Reminder }>( + `/api/v1/reminders/secrets/${secretId}` + ); + return data.reminder; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) }); + } + }); +}; + +export const useGetReminder = (secretId: string) => { + return useQuery({ + queryKey: reminderKeys.getReminder(secretId), + queryFn: async () => { + const { data } = await apiRequest.get<{ reminder: Reminder }>( + `/api/v1/reminders/secrets/${secretId}` + ); + return data.reminder; + }, + enabled: Boolean(secretId) + }); +}; diff --git a/frontend/src/hooks/api/reminders/types.ts b/frontend/src/hooks/api/reminders/types.ts new file mode 100644 index 000000000..245805aea --- /dev/null +++ b/frontend/src/hooks/api/reminders/types.ts @@ -0,0 +1,14 @@ +export type CreateReminderDTO = { + message?: string | null; + repeatDays?: number | null; + nextReminderDate?: Date | null; + secretId: string; + recipients?: string[]; +}; + +export type DeleteReminderDTO = { + secretId: string; + reminderId: string; +}; + +export type Reminder = { id: string } & CreateReminderDTO; 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..11de8cd62 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -25,7 +25,9 @@ export enum SecretSync { Supabase = "supabase", Zabbix = "zabbix", Railway = "railway", - Checkly = "checkly" + Checkly = "checkly", + DigitalOceanAppPlatform = "digital-ocean-app-platform", + 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/digital-ocean-app-platform-sync.ts b/frontend/src/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync.ts new file mode 100644 index 000000000..98d152531 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ +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 TDigitalOceanAppPlatformSync = TRootSecretSync & { + destination: SecretSync.DigitalOceanAppPlatform; + destinationConfig: { + appId: string; + appName: string; + }; + connection: { + app: AppConnection.DigitalOcean; + 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..7af01765e 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -8,11 +8,13 @@ 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"; import { TCloudflareWorkersSync } from "./cloudflare-workers-sync"; import { TDatabricksSync } from "./databricks-sync"; +import { TDigitalOceanAppPlatformSync } from "./digital-ocean-app-platform-sync"; import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; @@ -63,7 +65,9 @@ export type TSecretSync = | TZabbixSync | TRailwaySync | TChecklySync - | TSupabaseSync; + | TSupabaseSync + | TDigitalOceanAppPlatformSync + | 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..fa8c85b66 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -21,6 +21,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { ChecklyConnectionForm } from "./ChecklyConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; +import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm"; import { FlyioConnectionForm } from "./FlyioConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -33,6 +34,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 +151,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.DigitalOcean: + return ; + case AppConnection.Okta: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -253,6 +259,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.DigitalOcean: + 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/DigitalOceanConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DigitalOceanConnectionForm.tsx new file mode 100644 index 000000000..7663bd522 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DigitalOceanConnectionForm.tsx @@ -0,0 +1,136 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + DigitalOceanConnectionMethod, + TDigitalOceanConnection +} from "@app/hooks/api/appConnections/types/digital-ocean"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TDigitalOceanConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.DigitalOcean) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(DigitalOceanConnectionMethod.ApiToken), + credentials: z.object({ + apiToken: z.string().trim().min(1, "API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const DigitalOceanConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.DigitalOcean, + method: DigitalOceanConnectionMethod.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/OktaConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OktaConnectionForm.tsx new file mode 100644 index 000000000..0373d1e0e --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OktaConnectionForm.tsx @@ -0,0 +1,157 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { OktaConnectionMethod, TOktaConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TOktaConnection; + onSubmit: (formData: FormData) => 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/AuditLogsPage/AuditLogsPage.tsx b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx index bec3bfd77..25459719f 100644 --- a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx @@ -18,7 +18,7 @@ export const AuditLogsPage = () => { 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)} +
+
+ ))} +
+
( - -
+ + - + + { + navigator.clipboard.writeText(data.id); + + createNotification({ + text: "Copied ID to clipboard", + type: "info" + }); + }} + icon={} + > + Copy ID + + { + navigator.clipboard.writeText(data.slug); + + createNotification({ + text: "Copied slug to clipboard", + type: "info" + }); + }} + icon={} + > + Copy Slug + {(isAllowed) => ( { + onClick={() => { handlePopUpOpen("role", { roleId }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > Edit Role @@ -111,13 +146,10 @@ export const Page = () => { {(isAllowed) => ( { handlePopUpOpen("duplicateRole"); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > Duplicate Role @@ -126,15 +158,10 @@ export const Page = () => { {(isAllowed) => ( { + onClick={() => { handlePopUpOpen("deleteOrgRole"); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > Delete Role @@ -144,12 +171,7 @@ export const Page = () => { )} -
-
- -
- -
+
)} diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleDetailsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleDetailsSection.tsx deleted file mode 100644 index ee93ccfba..000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleDetailsSection.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { OrgPermissionCan } from "@app/components/permissions"; -import { IconButton, Tooltip } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; -import { useTimedReset } from "@app/hooks"; -import { useGetOrgRole } from "@app/hooks/api"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -type Props = { - roleId: string; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void; -}; - -export const RoleDetailsSection = ({ roleId, handlePopUpOpen }: Props) => { - const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ - initialState: "Copy ID to clipboard" - }); - - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - const { data } = useGetOrgRole(orgId, roleId); - const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? ""); - - return data ? ( -
-
-

Org Role Details

- {isCustomRole && ( - - {(isAllowed) => { - return ( - - - handlePopUpOpen("role", { - roleId - }) - } - > - - - - ); - }} - - )} -
-
-
-

Role ID

-
-

{roleId}

-
- - { - navigator.clipboard.writeText(roleId); - setCopyTextId("Copied"); - }} - > - - - -
-
-
-
-

Name

-

{data.name}

-
-
-

Slug

-

{data.slug}

-
-
-

Description

-

- {data.description?.length ? data.description : "-"} -

-
-
-
- ) : ( -
- ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx index 2c2ac5dce..38407fd98 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx @@ -76,17 +76,18 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" onClick={() => setIsRowExpanded.toggle()} > - - + + - Organization Admin Console + Organization Admin Console No Access Read Only @@ -145,11 +146,8 @@ export const OrgPermissionAppConnectionRow = ({ isEditable, control, setValue }: {isRowExpanded && ( - -
+ +
{PERMISSION_ACTIONS.map(({ action, label }) => { return ( setIsRowExpanded.toggle()} > - - + + - Billing + Billing No Access Read Only @@ -142,11 +143,8 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props {isRowExpanded && ( - -
+ +
{PERMISSION_ACTIONS.map(({ action, label }) => { return ( setIsRowExpanded.toggle()} > - - + + - Group Management + Group Management No Access Read Only @@ -176,11 +177,8 @@ export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Prop {isRowExpanded && ( - -
+ +
{PERMISSION_ACTIONS.map(({ action, label }) => { return ( setIsRowExpanded.toggle()} > - - + + - KMIP + KMIP No Access Custom @@ -89,11 +90,8 @@ export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: P {isRowExpanded && ( - -
+ +
{PERMISSION_ACTIONS.map(({ action, label }) => { return ( className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" onClick={() => setIsRowExpanded.toggle()} > - - + + - Project + Project No Access Read Only @@ -179,11 +180,8 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal {isRowExpanded && ( - -
+ +
{getPermissionList(formName).map(({ action, label }) => { return ( { className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" >
-

Permissions

+
+

Policies

+

Configure granular access policies

+
{isCustomRole && (
+ {isDirty && ( + + )} -
)}
- - - - - - {SIMPLE_PERMISSION_OPTIONS.map((permission) => { return ( diff --git a/frontend/src/pages/organization/RoleByIDPage/components/index.tsx b/frontend/src/pages/organization/RoleByIDPage/components/index.tsx index 3f57cd670..486c015ca 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/index.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/index.tsx @@ -1,3 +1,2 @@ -export { RoleDetailsSection } from "./RoleDetailsSection"; export { RoleModal } from "./RoleModal"; export { RolePermissionsSection } from "./RolePermissionsSection"; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx index 60ca359bb..439f71718 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx @@ -24,6 +24,7 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => { initialData: template, enabled: !isInfisicalTemplate }); + const finalTemplate = isInfisicalTemplate ? template : projectTemplate; return (
@@ -41,10 +42,10 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
- ) : projectTemplate ? ( + ) : finalTemplate ? ( ) : ( diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx index 50be108bc..c3ecdfa79 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx @@ -5,8 +5,10 @@ import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { getProjectTitle } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal"; import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm"; @@ -24,7 +26,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa "editDetails" ] as const); - const { id: templateId, name, description } = projectTemplate; + const { id: templateId, name, description, type } = projectTemplate; const deleteProjectTemplate = useDeleteProjectTemplate(); @@ -53,7 +55,10 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa

{name}

-

{description || "Project Template"}

+

+ {`${getProjectTitle(type)} - `} + {description || "Project Template"} +

{!isInfisicalTemplate && (
@@ -94,10 +99,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
)}
- + {type === ProjectType.SecretManager && ( + + )} Save - +
)} 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..3c5e138e3 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 Management", + value: ProjectType.SecretManager + }, + { + label: "Certificates Management", + 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}
+
+ ))} +
+
+ )} + /> {
+ - @@ -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) => { )} + - 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/DigitalOceanAppPlatformSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DigitalOceanAppPlatformSyncDestinationCol.tsx new file mode 100644 index 000000000..0dbf13ff8 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DigitalOceanAppPlatformSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TDigitalOceanAppPlatformSync } from "@app/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TDigitalOceanAppPlatformSync; +}; + +export const DigitalOceanAppPlatformSyncDestinationCol = ({ 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..f6848f748 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,11 +6,13 @@ 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"; import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; +import { DigitalOceanAppPlatformSyncDestinationCol } from "./DigitalOceanAppPlatformSyncDestinationCol"; import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -88,6 +90,10 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Supabase: return ; + case SecretSync.DigitalOceanAppPlatform: + 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..1ee3b141e 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,14 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectName; secondaryText = "Supabase Project"; break; + case SecretSync.DigitalOceanAppPlatform: + primaryText = destinationConfig.appName; + secondaryText = "Digital Ocean App"; + 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/SecretSearchInput.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx index 9c8562e06..9a69bfb9c 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx @@ -53,7 +53,7 @@ export const SecretSearchInput = ({ }} autoComplete="off" className="input text-md h-[2.3rem] w-full rounded-md rounded-l-none bg-mineshaft-800 py-[0.375rem] pl-2.5 pr-8 text-gray-400 placeholder-mineshaft-50 placeholder-opacity-50 outline-none duration-200 placeholder:text-sm hover:ring-bunker-400/60 focus:bg-mineshaft-700/80 focus:ring-1 focus:ring-primary-400/50" - placeholder="Search by secret/folder name..." + placeholder="Search by secret, folder, tag or metadata..." value={value} onChange={(e) => onChange(e.target.value)} /> 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..a9d592b1c 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 @@ -1,6 +1,7 @@ import { faCheck, faChevronRight, + faCode, faCopy, faEye, faFolder, @@ -46,7 +47,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 @@ -82,6 +83,17 @@ export const QuickSearchSecretItem = ({ search.trim() && secretGroupTags?.find((tag) => tag && tag.slug.toLowerCase().includes(search.toLowerCase())); + const secretGroupMetadata = secretGroup.flatMap((secret) => secret.secretMetadata); + + const metadataMatch = + search.trim() && + secretGroupMetadata?.find( + (metadata) => + metadata && + (metadata.key.toLowerCase().includes(search.toLowerCase()) || + metadata.value.toLowerCase().includes(search.toLowerCase())) + ); + return ( )} + {metadataMatch && !tagMatch && ( + + +

Metadata Match

+
+ )} {isSingleEnv ? ( { 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/CreateReminderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx index d5f1e146c..3b9db7bed 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx @@ -1,25 +1,65 @@ -import { useEffect } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { z } from "zod"; +import { createNotification } from "@app/components/notifications"; import { Button, + DatePicker, FilterableSelect, FormControl, Input, Modal, ModalContent, + Select, + SelectItem, TextArea } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetWorkspaceUsers } from "@app/hooks/api"; +import { dashboardKeys } from "@app/hooks/api/dashboard/queries"; +import { useCreateReminder, useDeleteReminder } from "@app/hooks/api/reminders"; +import { reminderKeys } from "@app/hooks/api/reminders/queries"; +import { Reminder } from "@app/hooks/api/reminders/types"; +import { secretKeys } from "@app/hooks/api/secrets/queries"; +// Constants +const MIN_REPEAT_DAYS = 1; +const MAX_REPEAT_DAYS = 365; +const DEFAULT_REPEAT_DAYS = 30; +const DEFAULT_TEXTAREA_ROWS = 8; + +// Enums +enum ReminderType { + Recurring = "Recurring", + OneTime = "One Time" +} + +// Types +interface RecipientOption { + label: string; + value: string; +} + +interface ReminderFormProps { + isOpen: boolean; + reminderId?: string; + onOpenChange: () => void; + workspaceId: string; + environment: string; + secretPath: string; + secretId: string; + reminder?: Reminder; +} + +// Validation Schema const ReminderFormSchema = z.object({ - note: z.string().optional().nullable(), + message: z.string().optional().nullable(), recipients: z .array( z.object({ @@ -28,182 +68,404 @@ const ReminderFormSchema = z.object({ }) ) .optional(), - days: z + repeatDays: z .number() - .min(1, { message: "Must be at least 1 day" }) - .max(365, { message: "Must be less than 365 days" }) + .min(MIN_REPEAT_DAYS, { message: `Must be at least ${MIN_REPEAT_DAYS} day` }) + .max(MAX_REPEAT_DAYS, { message: `Must be less than ${MAX_REPEAT_DAYS} days` }) .nullable() + .optional(), + nextReminderDate: z.coerce + .date() + .refine((data) => data > new Date(), { message: "Reminder date must be in the future" }) + .nullable() + .optional(), + reminderType: z.enum(["Recurring", "One Time"]) }); + export type TReminderFormSchema = z.infer; -interface ReminderFormProps { - isOpen: boolean; - repeatDays?: number | null; - note?: string | null; - recipients?: string[] | null; - onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void; -} +// Custom hook for form state management +const useReminderForm = (reminderData?: Reminder) => { + const { repeatDays, message, nextReminderDate } = reminderData || {}; + const isEditMode = Boolean(reminderData); + + const defaultValues = useMemo( + () => ({ + repeatDays: repeatDays || null, + message: message || "", + nextReminderDate: nextReminderDate || null, + reminderType: repeatDays ? ReminderType.Recurring : ReminderType.OneTime, + recipients: [] + }), + [repeatDays, message, nextReminderDate] + ); + + return { + isEditMode, + reminderData, + defaultValues + }; +}; + +// Custom hook for workspace members +const useWorkspaceMembers = () => { + const { currentWorkspace } = useWorkspace(); + const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id); + + const memberOptions = useMemo( + (): RecipientOption[] => + members.map((member) => ({ + label: member.user.username || member.user.email, + value: member.user.id + })), + [members] + ); + + return { members, memberOptions }; +}; + +// Main component export const CreateReminderForm = ({ isOpen, onOpenChange, - repeatDays, - note, - recipients + workspaceId, + environment, + secretPath, + secretId, + reminder }: ReminderFormProps) => { - const { currentWorkspace } = useWorkspace(); + const queryClient = useQueryClient(); + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false); - const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id); + // Custom hooks + const { isEditMode, reminderData } = useReminderForm(reminder); + const { memberOptions } = useWorkspaceMembers(); + + // API mutations + const { mutateAsync: createReminder } = useCreateReminder(secretId); + const { mutateAsync: deleteReminder } = useDeleteReminder(secretId); + + // Form setup + const form = useForm({ + defaultValues: { + repeatDays: reminderData?.repeatDays || null, + message: reminderData?.message || "", + nextReminderDate: reminderData?.nextReminderDate || null, + reminderType: reminderData?.repeatDays ? ReminderType.Recurring : ReminderType.OneTime, + recipients: [] + }, + resolver: zodResolver(ReminderFormSchema) + }); const { register, control, setValue, handleSubmit, + reset, + watch, formState: { isSubmitting } - } = useForm({ - defaultValues: { - days: repeatDays || undefined, - note: note || "" - }, - resolver: zodResolver(ReminderFormSchema) - }); + } = form; - const handleFormSubmit = async (data: TReminderFormSchema) => { - onOpenChange(false, data); + // Watch form values + const reminderType = watch("reminderType"); + + // Invalidate queries helper + const invalidateQueries = () => { + queryClient.invalidateQueries({ + queryKey: dashboardKeys.getDashboardSecrets({ + projectId: workspaceId, + secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: reminderKeys.getReminder(secretId) + }); }; - useEffect(() => { - // On initial load, filter the members to only include the recipients - if (members.length) { - const filteredMembers = members.filter((m) => recipients?.find((r) => r === m.user.id)); - setValue( - "recipients", - filteredMembers.map((m) => ({ - label: m.user.username || m.user.email, - value: m.user.id - })) - ); - } - }, [members, isOpen, recipients]); + // Form submission handler + const handleFormSubmit = async (data: TReminderFormSchema) => { + try { + await createReminder({ + repeatDays: data.repeatDays, + message: data.message, + recipients: data.recipients?.map((r) => r.value) || [], + secretId, + nextReminderDate: data.nextReminderDate + }); + invalidateQueries(); + + createNotification({ + type: "success", + text: `Successfully ${isEditMode ? "updated" : "created"} secret reminder` + }); + + reset(); + onOpenChange(); + } catch (error) { + console.error("Failed to save reminder:", error); + createNotification({ + type: "error", + text: "Failed to save reminder. Please try again." + }); + } + }; + + // Delete reminder handler + const handleDeleteReminder = async () => { + try { + await deleteReminder({ reminderId: reminder?.id || "", secretId }); + invalidateQueries(); + reset(); + onOpenChange(); + + createNotification({ + type: "success", + text: "Successfully deleted reminder" + }); + } catch (error) { + console.error("Failed to delete reminder:", error); + createNotification({ + type: "error", + text: "Failed to delete reminder. Please try again." + }); + } + }; + + // Handle reminder type change + const handleReminderTypeChange = useCallback( + (newType: string) => { + if (newType === ReminderType.Recurring) { + setValue("repeatDays", DEFAULT_REPEAT_DAYS); + setValue("nextReminderDate", null); + } else if (newType === ReminderType.OneTime) { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + setValue("nextReminderDate", tomorrow); + setValue("repeatDays", null); + } + }, + [setValue] + ); + + // Initialize form with existing data useEffect(() => { - if (repeatDays) setValue("days", repeatDays); - if (note) setValue("note", note); - }, [repeatDays, note]); + if (!reminderData) return; + + const { + repeatDays: repeatDaysInitial, + message, + recipients, + nextReminderDate: nextReminderDateInitial + } = reminderData; + + if (repeatDaysInitial) { + setValue("repeatDays", repeatDaysInitial); + setValue("reminderType", ReminderType.Recurring); + } else { + setValue("reminderType", ReminderType.OneTime); + } + + if (message) setValue("message", message); + if (nextReminderDateInitial) setValue("nextReminderDate", nextReminderDateInitial); + + // Set recipients + if (recipients?.length && memberOptions.length) { + const selectedRecipients = memberOptions.filter((option) => + recipients.includes(option.value) + ); + setValue("recipients", selectedRecipients); + } + }, [reminderData, memberOptions, setValue]); return ( -
-
-
- ( - <> - - setValue("days", parseInt(el.target.value, 10))} - type="number" - placeholder="31" - defaultValue={repeatDays || undefined} - value={field.value || undefined} - /> - -
- A reminder will be sent every{" "} - {field.value && field.value > 1 ? `${field.value} days` : "day"} -
- - )} - /> -
- - -
- ResourcePermission
NameType RolesEnvironments
{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..e400767a1 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 }, @@ -111,6 +112,32 @@ const Page = () => { + { + navigator.clipboard.writeText(data.id); + + createNotification({ + text: "Copied ID to clipboard", + type: "info" + }); + }} + icon={} + > + Copy ID + + { + navigator.clipboard.writeText(data.slug); + + createNotification({ + text: "Copied slug to clipboard", + type: "info" + }); + }} + icon={} + > + Copy Slug + { +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/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index c657ffd40..aa79aee69 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -124,7 +124,7 @@ export const GeneralPermissionPolicies = +
    - + -
    {title}
    +
    {title}
    {fields.length > 1 && (
    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..4dbf84d92 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -4,7 +4,6 @@ import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; import { faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { AccessTree } from "@app/components/permissions"; @@ -14,6 +13,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 +121,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( @@ -163,7 +165,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
    - +
    )} @@ -206,7 +208,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/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 a1d058ea3..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,9 +1,11 @@ /* 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; version: number; @@ -33,6 +35,7 @@ interface SecretVersionDiffViewProps { showHeader?: boolean; customHeader?: JSX.Element; excludedFieldsHighlight?: string[]; + onDiscard?: VoidFunction; } const isObject = (obj: JsonValue): obj is JsonObject => { @@ -409,7 +412,6 @@ const renderJsonWithDiffs = (
    {indent} {"}"} - {comma}
    @@ -465,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 = ({ @@ -474,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); @@ -609,6 +614,20 @@ export const SecretVersionDiffView = ({

    {key}

    {changeBadge} + {onDiscard && ( + + + + + + )} ({ 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 591c03d4d..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, 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 = ({