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 6f3e3029d..185a32356 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -509,6 +509,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 { TSecretApprovalPoliciesEnvironments, TSecretApprovalPoliciesEnvironmentsInsert, @@ -1232,5 +1238,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/schemas/models.ts b/backend/src/db/schemas/models.ts index 8ea73cdf4..55ec12faa 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -162,7 +162,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", @@ -174,7 +174,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"; @@ -269,6 +272,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/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/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/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 0282175df..d6187d418 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"; @@ -121,7 +122,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -291,7 +293,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); @@ -368,7 +371,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: accessApprovalPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -573,7 +577,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorId, projectId: policy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -626,7 +631,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" }); @@ -667,7 +673,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 03dd9e7de..e9b311905 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"; @@ -126,7 +126,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" }); @@ -235,7 +236,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: { @@ -308,7 +309,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" }); @@ -368,7 +370,8 @@ export const accessApprovalRequestServiceFactory = ({ actorId, projectId: accessApprovalRequest.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!membership) { @@ -584,7 +587,7 @@ export const accessApprovalRequestServiceFactory = ({ bypassReason: bypassReason || "No reason provided", secretPath: policy.secretPath || "/", environment: environment?.name || permissionEnvironment, - 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 @@ -615,7 +618,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 83458e691..818bb1f99 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -468,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[] = [ @@ -3326,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 @@ -3689,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 782827c98..ef7f9b5a3 100644 --- a/backend/src/ee/services/pit/pit-service.ts +++ b/backend/src/ee/services/pit/pit-service.ts @@ -1,6 +1,7 @@ /* 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 { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -348,7 +349,8 @@ export const pitServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(userPermission).throwUnlessCan( 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 e3d54bcf5..2bff6f440 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"; @@ -116,7 +117,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -330,7 +332,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId: secretApprovalPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -497,7 +500,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId: sapPolicy.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -541,7 +545,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); @@ -585,7 +590,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); return getSecretApprovalPolicy(projectId, environment, secretPath); @@ -611,7 +617,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 75efa948c..8e366fefc 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 @@ -3,6 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { Knex } from "knex"; import { + ActionProjectType, ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, @@ -184,7 +185,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId, policyId); @@ -211,7 +213,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -263,7 +266,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -412,7 +416,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId: secretApprovalRequest.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -481,7 +486,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId: secretApprovalRequest.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -542,7 +548,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if ( @@ -960,7 +967,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 }); @@ -1094,7 +1101,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { @@ -1385,7 +1393,8 @@ export const secretApprovalRequestServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts index 38ac137dc..765ca3ab3 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts @@ -167,7 +167,7 @@ export const secretRotationV2QueueServiceFactory = async ({ environment: environment.name, projectName: project.name, rotationUrl: encodeURI( - `${appCfg.SITE_URL}/projects/${projectId}/secret-manager/secrets/${environment.slug}` + `${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}` ) } }); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index 334a9e9e8..65f60972f 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { Knex } from "knex"; import isEqual from "lodash.isequal"; -import { SecretType, TableName } from "@app/db/schemas"; +import { ActionProjectType, SecretType, TableName } from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -223,7 +223,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -274,7 +274,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -320,7 +320,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -385,7 +385,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -429,7 +429,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -631,7 +631,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -781,7 +781,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1113,7 +1113,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1160,7 +1160,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1212,7 +1212,7 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - + actionProjectType: ActionProjectType.SecretManager, projectId }); @@ -1328,7 +1328,8 @@ export const secretRotationV2ServiceFactory = ({ actorId: actor.id, projectId, actorAuthMethod: actor.authMethod, - actorOrgId: actor.orgId + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager }); const permissiveFolderMappings = folderMappings.filter(({ path, environment }) => diff --git a/backend/src/ee/services/secret-rotation/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 1550ef41a..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 @@ -596,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 } @@ -607,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 797cf3fae..b6c00985a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2290,6 +2290,9 @@ export const AppConnections = { 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." @@ -2506,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." } } }; 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 f3f79260f..b2b0c9924 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -248,6 +248,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"; @@ -373,6 +377,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); @@ -739,9 +746,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, @@ -767,7 +782,8 @@ export const registerRoutes = async ( orgBotDAL, oidcConfigDAL, loginService, - projectBotService + projectBotService, + reminderService }); const signupService = authSignupServiceFactory({ tokenService, @@ -1065,7 +1081,6 @@ export const registerRoutes = async ( secretImportDAL, projectEnvDAL, webhookDAL, - orgDAL, auditLogService, userDAL, projectMembershipDAL, @@ -1087,11 +1102,11 @@ export const registerRoutes = async ( secretApprovalRequestDAL, projectKeyDAL, projectUserMembershipRoleDAL, - secretReminderRecipientsDAL, orgService, resourceMetadataDAL, folderCommitService, - secretSyncQueue + secretSyncQueue, + reminderService }); const projectService = projectServiceFactory({ @@ -1100,7 +1115,6 @@ export const registerRoutes = async ( projectSshConfigDAL, secretDAL, secretV2BridgeDAL, - queueService, projectQueue: projectQueueService, projectBotService, identityProjectDAL, @@ -1137,7 +1151,8 @@ export const registerRoutes = async ( microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, - smtpService + smtpService, + reminderService }); const projectEnvService = projectEnvServiceFactory({ @@ -1238,6 +1253,7 @@ export const registerRoutes = async ( kmsService, snapshotService, resourceMetadataDAL, + reminderService, keyStore }); @@ -1291,7 +1307,8 @@ export const registerRoutes = async ( secretApprovalRequestSecretDAL, secretV2BridgeService, secretApprovalRequestService, - licenseService + licenseService, + reminderService }); const secretSharingService = secretSharingServiceFactory({ @@ -1624,7 +1641,6 @@ export const registerRoutes = async ( auditLogDAL, queueService, secretVersionDAL, - secretDAL, secretFolderVersionDAL: folderVersionDAL, snapshotDAL, identityAccessTokenDAL, @@ -1635,6 +1651,13 @@ export const registerRoutes = async ( orgService }); + const dailyReminderQueueService = dailyReminderQueueServiceFactory({ + reminderService, + queueService, + secretDAL: secretV2BridgeDAL, + secretReminderRecipientsDAL + }); + const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ queueService, pkiAlertService @@ -1934,6 +1957,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(); @@ -2044,7 +2069,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 ba0826f87..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"; @@ -140,6 +144,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedRailwayConnectionSchema.options, ...SanitizedChecklyConnectionSchema.options, ...SanitizedSupabaseConnectionSchema.options, + ...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedOktaConnectionSchema.options ]); @@ -178,6 +183,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ RailwayConnectionListItemSchema, ChecklyConnectionListItemSchema, SupabaseConnectionListItemSchema, + DigitalOceanConnectionListItemSchema, OktaConnectionListItemSchema ]); 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 3bbdc363d..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"; @@ -74,5 +75,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { 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 7fcdc7217..833522cbb 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -33,6 +33,7 @@ export enum AppConnection { Bitbucket = "bitbucket", Checkly = "checkly", Supabase = "supabase", + DigitalOcean = "digital-ocean", Okta = "okta" } diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 9568761f7..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"; @@ -157,6 +162,7 @@ export const listAppConnectionOptions = () => { getBitbucketConnectionListItem(), getChecklyConnectionListItem(), getSupabaseConnectionListItem(), + getDigitalOceanConnectionListItem(), getOktaConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -244,6 +250,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -283,6 +290,7 @@ 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: @@ -372,6 +380,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Checkly]: platformManagedCredentialsNotSupported, [AppConnection.Supabase]: platformManagedCredentialsNotSupported, + [AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 03b979312..603d30ccd 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -35,6 +35,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Checkly]: "Checkly", [AppConnection.Supabase]: "Supabase", + [AppConnection.DigitalOcean]: "DigitalOcean App Platform", [AppConnection.Okta]: "Okta" }; @@ -73,5 +74,6 @@ export const APP_CONNECTION_PLAN_MAP: Record { }; }; +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/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 3612cc005..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"; @@ -219,7 +225,8 @@ export const folderCommitServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits); @@ -2062,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-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 b76e93fed..7fbe7343e 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 { TAccessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -52,7 +53,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); @@ -140,7 +142,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); @@ -203,7 +206,8 @@ export const projectEnvServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); @@ -272,7 +276,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 030bbbf09..a60d29348 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -4,7 +4,7 @@ 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"; @@ -78,7 +78,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -269,7 +270,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); folders.forEach(({ environment, path: secretPath }) => { @@ -411,7 +413,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -612,7 +615,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( @@ -718,7 +722,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); @@ -786,7 +791,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -827,7 +833,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -862,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]); @@ -890,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); @@ -917,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 }); @@ -1019,7 +1029,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); folders.forEach(({ environment, path: secretPath }) => { @@ -1230,7 +1241,8 @@ export const secretFolderServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); folders.forEach(({ environment, path: secretPath }) => { 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 fae3a07d0..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 @@ -231,18 +231,7 @@ export const fnSecretBulkUpdate = async ({ const sanitizedInputSecrets = inputSecrets.map( ({ filter, - data: { - skipMultilineEncoding, - type, - key, - encryptedValue, - userId, - encryptedComment, - metadata, - secretMetadata, - reminderNote, - reminderRepeatDays - } + data: { skipMultilineEncoding, type, key, encryptedValue, userId, encryptedComment, metadata, secretMetadata } }) => ({ filter: { ...filter, folderId }, data: { @@ -252,9 +241,7 @@ export const fnSecretBulkUpdate = async ({ userId, encryptedComment, metadata: JSON.stringify(metadata || secretMetadata || []), - reminderNote, - encryptedValue, - reminderRepeatDays + encryptedValue } }) ); @@ -270,9 +257,7 @@ export const fnSecretBulkUpdate = async ({ encryptedComment, version, metadata, - reminderNote, encryptedValue, - reminderRepeatDays, id: secretId }) => ({ skipMultilineEncoding, @@ -282,9 +267,7 @@ export const fnSecretBulkUpdate = async ({ encryptedComment, version, metadata: metadata ? JSON.stringify(metadata) : [], - reminderNote, encryptedValue, - reminderRepeatDays, folderId, secretId, userActorId, @@ -407,6 +390,7 @@ export const fnSecretBulkDelete = async ({ secretQueueService, folderCommitService, secretVersionDAL, + projectId, commitChanges }: TFnSecretBulkDelete) => { const deletedSecrets = await secretDAL.deleteMany( @@ -419,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 + ) ) ); 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 8f082df03..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 @@ -32,6 +39,7 @@ import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder 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); @@ -1514,7 +1544,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -1689,7 +1720,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const secretsToUpdateGroupByPath = groupBy(inputSecrets, (el) => el.secretPath || defaultSecretPath); @@ -1912,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, @@ -2050,7 +2080,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -2211,7 +2242,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId: folder.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const canRead = @@ -2276,7 +2308,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); if (!hasRole(ProjectMembershipRole.Admin)) @@ -2323,7 +2356,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); @@ -2579,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, @@ -2709,7 +2741,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { @@ -2802,7 +2835,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, @@ -2926,7 +2960,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager }); const canRead = @@ -2972,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, @@ -2991,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 825e7c805..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 @@ -249,6 +249,7 @@ export type TCreateSecretReminderDTO = { export type TRemoveSecretReminderDTO = { secretId: string; repeatDays: number; + projectId: string; }; export type TBackFillSecretReferencesDTO = TProjectPermission; @@ -358,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 2d52344e5..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; }; 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/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 48182dcf9..13ec5367e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -476,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", @@ -516,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", @@ -1381,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", @@ -1749,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": [ @@ -1814,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": [ @@ -2270,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/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/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-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/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/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 { 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/DatePicker/DatePicker.tsx b/frontend/src/components/v2/DatePicker/DatePicker.tsx index a4af1afd9..115b0ad3e 100644 --- a/frontend/src/components/v2/DatePicker/DatePicker.tsx +++ b/frontend/src/components/v2/DatePicker/DatePicker.tsx @@ -20,6 +20,7 @@ export type DatePickerProps = Omit & { popUpProps: PopoverProps; popUpContentProps: PopoverContentProps; dateFormat?: "PPP" | "PP" | "P"; // extend as needed + hideTime?: boolean; buttonClassName?: string; timezone?: Timezone; }; @@ -55,6 +56,7 @@ export const DatePicker = ({ popUpProps, popUpContentProps, dateFormat = "PPP", + hideTime = false, buttonClassName, timezone, ...props @@ -124,14 +126,16 @@ export const DatePicker = ({ }} />
-
- -
+ {!hideTime && ( +
+ +
+ )} ); 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 807569c6e..1345cb493 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -17,9 +17,7 @@ import { AzureClientSecretsConnectionMethod, AzureDevOpsConnectionMethod, AzureKeyVaultConnectionMethod, - BitbucketConnectionMethod, CamundaConnectionMethod, - ChecklyConnectionMethod, CloudflareConnectionMethod, DatabricksConnectionMethod, FlyioConnectionMethod, @@ -28,19 +26,14 @@ import { GitHubRadarConnectionMethod, GitLabConnectionMethod, HCVaultConnectionMethod, - HerokuConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, - OCIConnectionMethod, OktaConnectionMethod, OnePassConnectionMethod, OracleDBConnectionMethod, PostgresConnectionMethod, - RailwayConnectionMethod, - RenderConnectionMethod, - SupabaseConnectionMethod, TAppConnection, TeamCityConnectionMethod, TerraformCloudConnectionMethod, @@ -48,6 +41,14 @@ import { WindmillConnectionMethod, ZabbixConnectionMethod } from "@app/hooks/api/appConnections/types"; +import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; +import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; +import { 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"; +import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; +import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -100,6 +101,10 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" }, [AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }, + [AppConnection.DigitalOcean]: { + name: "Digital Ocean", + image: "Digital Ocean.png" + }, [AppConnection.Okta]: { name: "Okta", image: "Okta.png" } }; @@ -134,6 +139,7 @@ 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: 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/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 2e1e59655..c3a1fec05 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -33,5 +33,6 @@ export enum AppConnection { Railway = "railway", Checkly = "checkly", Supabase = "supabase", + DigitalOcean = "digital-ocean", Okta = "okta" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index e9bc5b243..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,10 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Supabase; }; +export type TDigitalOceanConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.DigitalOcean; +}; + export type TOktaConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Okta; }; @@ -188,6 +192,7 @@ export type TAppConnectionOption = | TZabbixConnectionOption | TRailwayConnectionOption | TChecklyConnectionOption + | TDigitalOceanConnectionOption | TOktaConnectionOption; export type TAppConnectionOptionMap = { @@ -225,5 +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 5085feab3..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"; @@ -107,6 +108,7 @@ export type TAppConnection = | TRailwayConnection | TChecklyConnection | TSupabaseConnection + | TDigitalOceanConnection | TOktaConnection; export type TAvailableAppConnection = Pick; @@ -178,5 +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/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/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/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/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 b1181a68b..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"; @@ -150,6 +151,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.DigitalOcean: + return ; case AppConnection.Okta: return ; default: @@ -256,6 +259,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.DigitalOcean: + return ; case AppConnection.Okta: return ; default: 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/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/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index 8d8d45445..5b0aac0b7 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -6,6 +6,7 @@ import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { getProjectBaseURL } from "@app/helpers/project"; import { formatProjectRoleName } from "@app/helpers/roles"; import { useGetUserWorkspaces } from "@app/hooks/api"; import { IdentityMembership } from "@app/hooks/api/identities/types"; @@ -50,7 +51,7 @@ export const IdentityProjectRow = ({ onClick={() => { 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/organization/ProjectsPage/ProjectsPage.tsx b/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx index 48f6e02f0..f9131ad66 100644 --- a/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx @@ -38,7 +38,8 @@ export const ProjectsPage = () => { "upgradePlan" ] as const); - const { data: serverDetails } = useFetchServerStatus(); + const { data: serverDetails, isLoading } = useFetchServerStatus(); + const { subscription } = useSubscription(); const isAddingProjectsAllowed = subscription?.workspaceLimit @@ -51,7 +52,8 @@ export const ProjectsPage = () => { {t("common.head-title", { title: t("settings.members.title") })} - {!serverDetails?.redisConfigured && ( + + {!isLoading && !serverDetails?.redisConfigured && (

Announcements

diff --git a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx index 3789cf024..52d8e5368 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx @@ -4,6 +4,7 @@ import { faArrowDownAZ, faBorderAll, faCheck, + faCheckCircle, faFolderOpen, faList, faMagnifyingGlass, @@ -16,10 +17,17 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Badge, Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, FormControl, IconButton, Input, + Lottie, Modal, ModalContent, Pagination, @@ -27,7 +35,7 @@ import { Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; -import { getProjectHomePage } from "@app/helpers/project"; +import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project"; import { getUserTablePreference, PreferenceKey, @@ -35,7 +43,7 @@ import { } from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; type Props = { onAddNewProject: () => void; @@ -98,6 +106,7 @@ export const AllProjectView = ({ const navigate = useNavigate(); const [searchFilter, setSearchFilter] = useState(""); const [debouncedSearch] = useDebounce(searchFilter); + const [projectTypeFilter, setProjectTypeFilter] = useState(); const { setPage, perPage, @@ -124,7 +133,8 @@ export const AllProjectView = ({ limit, offset, name: debouncedSearch || undefined, - orderDirection + orderDirection, + type: projectTypeFilter }); useResetPageHelper({ @@ -134,6 +144,9 @@ export const AllProjectView = ({ }); const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Workspace; + const handleToggleFilterByProjectType = (el: ProjectType) => + setProjectTypeFilter((state) => (state === el ? undefined : el)); + return (
@@ -160,16 +173,62 @@ export const AllProjectView = ({
+ + +
+ + + + + +
+
+ + Filter By Project Type + {Object.values(ProjectType).map((el) => ( + { + e.preventDefault(); + handleToggleFilterByProjectType(el); + }} + icon={projectTypeFilter === el && } + iconPos="right" + > +
+ {getProjectTitle(el)} +
+
+ ))} +
+
- - - +
+ + + +
( -
- )} -
+
+
+
-
- {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/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 b54fce0cc..3342f6688 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -234,7 +234,7 @@ const Page = () => { type: "error" }); navigate({ - to: "/projects/$projectId/secret-manager/overview", + to: "/projects/secret-management/$projectId/overview", params: { projectId: workspaceId } @@ -386,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, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx index b961775d3..c2e3ab12b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ import React, { useCallback, useState } from "react"; -import { faCodeCommit, faEye, faFolder, faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; +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"; @@ -296,39 +296,46 @@ export const CommitForm: React.FC = ({ <> {/* Floating Panel */} {!isModalOpen && ( -
-
-
+
+
+ {/* Left Content */} +
+ {/* Header */}
- +
Pending Changes + + {totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""} +
- - {totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""} - -
-
-
- - + {/* Description */} +

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

+
+ + {/* Right Buttons */} +
+ + +
)} 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/route.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx index 623b42050..0cd791c27 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/route.tsx @@ -12,7 +12,7 @@ const CommitDetailsPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/" + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/" )({ component: CommitDetailsPage, validateSearch: zodValidator(CommitDetailsPageQueryParamsSchema), @@ -33,7 +33,7 @@ export const Route = createFileRoute( links: context.project.environments.map((el) => ({ label: el.name, link: linkOptions({ - to: "/projects/$projectId/secret-manager/secrets/$envSlug", + to: "/projects/secret-management/$projectId/secrets/$envSlug", params: { projectId: params.projectId, envSlug: el.slug @@ -56,7 +56,7 @@ export const Route = createFileRoute( { label: "Commits", link: linkOptions({ - to: "/projects/$projectId/secret-manager/commits/$environment/$folderId", + to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { projectId: params.projectId, environment: params.environment, diff --git a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx index 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 = ({