diff --git a/backend/src/db/migrations/20240718170955_add-access-secret-sharing.ts b/backend/src/db/migrations/20240718170955_add-access-secret-sharing.ts new file mode 100644 index 000000000..eb053cf80 --- /dev/null +++ b/backend/src/db/migrations/20240718170955_add-access-secret-sharing.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { SecretSharingAccessType } from "@app/lib/types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType"); + if (!hasColumn) { + await knex.schema.table(TableName.SecretSharing, (table) => { + table.string("accessType", 20).notNullable().defaultTo(SecretSharingAccessType.Anyone); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType"); + if (hasColumn) { + await knex.schema.table(TableName.SecretSharing, (table) => { + table.dropColumn("accessType"); + }); + } +} diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index c8d938861..4406ad493 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { SecretSharingAccessType } from "@app/lib/types"; + import { TImmutableDBKeys } from "./models"; export const SecretSharingSchema = z.object({ @@ -16,6 +18,7 @@ export const SecretSharingSchema = z.object({ expiresAt: z.date(), userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(), + accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization), createdAt: z.date(), updatedAt: z.date(), expiresAfterViews: z.number().nullable().optional() diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 382762aaa..4d892b02c 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -47,3 +47,8 @@ export enum EnforcementLevel { Hard = "hard", Soft = "soft" } + +export enum SecretSharingAccessType { + Anyone = "anyone", + Organization = "organization" +} diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a0df46dc5..3424e7134 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -737,7 +737,8 @@ export const registerRoutes = async ( const secretSharingService = secretSharingServiceFactory({ permissionService, - secretSharingDAL + secretSharingDAL, + orgDAL }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 4ec2737fb..9e5fe1f79 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretSharingSchema } from "@app/db/schemas"; +import { SecretSharingAccessType } from "@app/lib/types"; import { publicEndpointLimit, publicSecretShareCreationLimit, @@ -55,14 +56,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => iv: true, tag: true, expiresAt: true, - expiresAfterViews: true + expiresAfterViews: true, + accessType: true + }).extend({ + orgName: z.string().optional() }) } }, handler: async (req) => { const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex( req.params.id, - req.query.hashedHex + req.query.hashedHex, + req.permission?.orgId ); if (!sharedSecret) return undefined; return { @@ -70,7 +75,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => iv: sharedSecret.iv, tag: sharedSecret.tag, expiresAt: sharedSecret.expiresAt, - expiresAfterViews: sharedSecret.expiresAfterViews + expiresAfterViews: sharedSecret.expiresAfterViews, + accessType: sharedSecret.accessType, + orgName: sharedSecret.orgName }; } }); @@ -88,7 +95,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag: z.string(), hashedHex: z.string(), expiresAt: z.string(), - expiresAfterViews: z.number() + expiresAfterViews: z.number(), + accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) }), response: { 200: z.object({ @@ -97,14 +105,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body; + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = req.body; const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({ encryptedValue, iv, tag, hashedHex, expiresAt: new Date(expiresAt), - expiresAfterViews + expiresAfterViews, + accessType }); return { id: sharedSecret.id }; } @@ -123,7 +132,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag: z.string(), hashedHex: z.string(), expiresAt: z.string(), - expiresAfterViews: z.number() + expiresAfterViews: z.number(), + accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) }), response: { 200: z.object({ @@ -145,7 +155,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag, hashedHex, expiresAt: new Date(expiresAt), - expiresAfterViews + expiresAfterViews, + accessType: req.body.accessType }); return { id: sharedSecret.id }; } diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index e40b4ef26..a4e95af49 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,6 +1,8 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { SecretSharingAccessType } from "@app/lib/types"; +import { TOrgDALFactory } from "../org/org-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { TCreatePublicSharedSecretDTO, @@ -12,13 +14,15 @@ import { type TSecretSharingServiceFactoryDep = { permissionService: Pick; secretSharingDAL: TSecretSharingDALFactory; + orgDAL: TOrgDALFactory; }; export type TSecretSharingServiceFactory = ReturnType; export const secretSharingServiceFactory = ({ permissionService, - secretSharingDAL + secretSharingDAL, + orgDAL }: TSecretSharingServiceFactoryDep) => { const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { const { @@ -30,6 +34,7 @@ export const secretSharingServiceFactory = ({ encryptedValue, iv, tag, + accessType, hashedHex, expiresAt, expiresAfterViews @@ -62,13 +67,14 @@ export const secretSharingServiceFactory = ({ expiresAt, expiresAfterViews, userId: actorId, - orgId + orgId, + accessType }); return { id: newSharedSecret.id }; }; const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => { - const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput; + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput; if (new Date(expiresAt) < new Date()) { throw new BadRequestError({ message: "Expiration date cannot be in the past" }); } @@ -92,7 +98,8 @@ export const secretSharingServiceFactory = ({ tag, hashedHex, expiresAt, - expiresAfterViews + expiresAfterViews, + accessType }); return { id: newSharedSecret.id }; }; @@ -105,9 +112,21 @@ export const secretSharingServiceFactory = ({ return userSharedSecrets; }; - const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => { + const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string, orgId?: string) => { const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex }); if (!sharedSecret) return; + + const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; + // Support organization level access for secret sharing + if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) { + return { + ...sharedSecret, + encryptedValue: "", + iv: "", + tag: "", + orgName + }; + } if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) { return; } @@ -118,7 +137,10 @@ export const secretSharingServiceFactory = ({ } await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); } - return sharedSecret; + if (sharedSecret.accessType === SecretSharingAccessType.Organization) { + return { ...sharedSecret, orgName }; + } + return { ...sharedSecret, orgName: undefined }; }; const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 769bb4479..a9c7dcbd9 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -1,3 +1,5 @@ +import { SecretSharingAccessType } from "@app/lib/types"; + import { ActorAuthMethod, ActorType } from "../auth/auth-type"; export type TSharedSecretPermission = { @@ -6,6 +8,7 @@ export type TSharedSecretPermission = { actorAuthMethod: ActorAuthMethod; actorOrgId: string; orgId: string; + accessType?: SecretSharingAccessType; }; export type TCreatePublicSharedSecretDTO = { @@ -15,6 +18,7 @@ export type TCreatePublicSharedSecretDTO = { hashedHex: string; expiresAt: Date; expiresAfterViews: number; + accessType: SecretSharingAccessType; }; export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;