diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index bc2cbc969..b8c88bd26 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -14,6 +14,9 @@ import requireServiceAccountAuth from "./requireServiceAccountAuth"; import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth"; import requireSecretAuth from "./requireSecretAuth"; import requireSecretsAuth from "./requireSecretsAuth"; +import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled"; +import requireE2EEOff from "./requireE2EEOff"; +import requireIPAllowlistCheck from "./requireIPAllowlistCheck"; import validateRequest from "./validateRequest"; export { @@ -33,5 +36,8 @@ export { requireServiceAccountWorkspacePermissionAuth, requireSecretAuth, requireSecretsAuth, + requireBlindIndicesEnabled, + requireE2EEOff, + requireIPAllowlistCheck, validateRequest, }; diff --git a/backend/src/middleware/requireBlindIndicesEnabled.ts b/backend/src/middleware/requireBlindIndicesEnabled.ts new file mode 100644 index 000000000..b1288dd2b --- /dev/null +++ b/backend/src/middleware/requireBlindIndicesEnabled.ts @@ -0,0 +1,34 @@ +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { SecretBlindIndexData } from "../models"; +import { UnauthorizedRequestError } from "../utils/errors"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has blind indices enabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireBlindIndicesEnabled = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + const secretBlindIndexData = await SecretBlindIndexData.exists({ + workspace: new Types.ObjectId(workspaceId) + }); + + if (!secretBlindIndexData) throw UnauthorizedRequestError({ + message: "Failed workspace authorization due to blind indices not being enabled" + }); + + return next(); + } +} + +export default requireBlindIndicesEnabled; \ No newline at end of file diff --git a/backend/src/middleware/requireE2EEOff.ts b/backend/src/middleware/requireE2EEOff.ts new file mode 100644 index 000000000..a9e5a735b --- /dev/null +++ b/backend/src/middleware/requireE2EEOff.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from "express"; +import { BadRequestError } from "../utils/errors"; +import { BotService } from "../services"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has E2EE off/disabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireE2EEOff = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, _: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); + + if (isWorkspaceE2EE) throw BadRequestError({ + message: "Failed workspace authorization due to end-to-end encryption not being disabled" + }); + + return next(); + } +} + +export default requireE2EEOff; \ No newline at end of file diff --git a/backend/src/middleware/requireIPAllowlistCheck.ts b/backend/src/middleware/requireIPAllowlistCheck.ts new file mode 100644 index 000000000..411c802a9 --- /dev/null +++ b/backend/src/middleware/requireIPAllowlistCheck.ts @@ -0,0 +1,55 @@ +import net from "net"; +import { NextFunction, Request, Response } from "express"; +import { UnauthorizedRequestError } from "../utils/errors"; +import { extractIPDetails } from "../utils/ip"; +import { ActorType, TrustedIP } from "../ee/models"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has E2EE off/disabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireIPAllowlistCheck = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, _: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + if (req.authData.actor.type === ActorType.SERVICE) { + const trustedIps = await TrustedIP.find({ + workspace: workspaceId + }); + + if (trustedIps.length > 0) { + // case: check the IP address of the inbound request against trusted IPs + + const blockList = new net.BlockList(); + + for (const trustedIp of trustedIps) { + if (trustedIp.prefix !== undefined) { + blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type); + } else { + blockList.addAddress(trustedIp.ipAddress, trustedIp.type); + } + } + + const { type } = extractIPDetails(req.authData.ipAddress); + const check = blockList.check(req.authData.ipAddress, type); + + if (!check) + throw UnauthorizedRequestError({ + message: "Failed workspace authorization" + }); + } + } + + return next(); + } +} + +export default requireIPAllowlistCheck; \ No newline at end of file diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index f6f7405a9..bbf829565 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -9,24 +9,18 @@ type req = "params" | "body" | "query"; * on request params. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth - * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireWorkspaceAuth = ({ acceptedRoles, locationWorkspaceId, locationEnvironment = undefined, requiredPermissions = [], - requireBlindIndicesEnabled = false, - requireE2EEOff = false, - checkIPAllowlist = false }: { acceptedRoles: Array<"admin" | "member">; locationWorkspaceId: req; locationEnvironment?: req | undefined; requiredPermissions?: string[]; - requireBlindIndicesEnabled?: boolean; - requireE2EEOff?: boolean; - checkIPAllowlist?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { const workspaceId = req[locationWorkspaceId]?.workspaceId; @@ -38,10 +32,7 @@ const requireWorkspaceAuth = ({ workspaceId: new Types.ObjectId(workspaceId), environment, acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled, - requireE2EEOff, - checkIPAllowlist + requiredPermissions }); if (membership) { diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 5d6b8f772..338c72fe7 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,6 +1,12 @@ import express from "express"; const router = express.Router(); -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; +import { + requireAuth, + requireBlindIndicesEnabled, + requireE2EEOff, + requireWorkspaceAuth, + validateRequest +} from "../../middleware"; import { body, param, query } from "express-validator"; import { secretsController } from "../../controllers/v3"; import { @@ -21,8 +27,6 @@ router.get( secretsController.getSecretsRaw ); -// TODO(akhilmhdh): tony please split the requireWorkspaceAuth to multiple middlewares -// IP checking into another one router.get( "/raw/:secretName", requireAuth({ @@ -32,10 +36,13 @@ router.get( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "query", locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_READ_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" + }), + requireE2EEOff({ + locationWorkspaceId: "query" }), secretsController.getSecretByNameRaw ); @@ -49,10 +56,13 @@ router.post( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.createSecretRaw ); @@ -66,10 +76,13 @@ router.patch( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.updateSecretByNameRaw ); @@ -83,10 +96,13 @@ router.delete( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.deleteSecretByNameRaw ); @@ -105,10 +121,10 @@ router.get( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "query", locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_READ_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" }), secretsController.getSecrets ); @@ -122,10 +138,10 @@ router.post( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.createSecret ); @@ -139,9 +155,10 @@ router.get( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "query", locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_READ_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" }), secretsController.getSecretByName ); @@ -155,10 +172,10 @@ router.patch( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.updateSecretByName ); @@ -178,10 +195,10 @@ router.delete( acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }), + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.deleteSecretByName ); diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index d6a42a55f..44044d923 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -1,13 +1,10 @@ -import net from "net"; import { Types } from "mongoose"; -import { IServiceTokenData, IUser, SecretBlindIndexData, Workspace } from "../models"; -import { ActorType, TrustedIP } from "../ee/models"; +import { IServiceTokenData, IUser, Workspace } from "../models"; +import { ActorType } from "../ee/models"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; -import { BadRequestError, UnauthorizedRequestError, WorkspaceNotFoundError } from "../utils/errors"; -import { BotService } from "../services"; +import { WorkspaceNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; -import { extractIPDetails } from "../utils/ip"; import { z } from "zod"; import { EventType, UserAgentType } from "../ee/models"; @@ -26,50 +23,19 @@ export const validateClientForWorkspace = async ({ workspaceId, environment, acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled, - requireE2EEOff, - checkIPAllowlist + requiredPermissions }: { authData: AuthData; workspaceId: Types.ObjectId; environment?: string; acceptedRoles: Array<"admin" | "member">; requiredPermissions?: string[]; - requireBlindIndicesEnabled: boolean; - requireE2EEOff: boolean; - checkIPAllowlist: boolean; }) => { const workspace = await Workspace.findById(workspaceId); - if (!workspace) - throw WorkspaceNotFoundError({ - message: "Failed to find workspace" - }); - - if (requireBlindIndicesEnabled) { - // case: blind indices are not enabled for secrets in this workspace - // (i.e. workspace was created before blind indices were introduced - // and no admin has enabled it) - - const secretBlindIndexData = await SecretBlindIndexData.exists({ - workspace: new Types.ObjectId(workspaceId) - }); - - if (!secretBlindIndexData) - throw UnauthorizedRequestError({ - message: "Failed workspace authorization due to blind indices not being enabled" - }); - } - - if (requireE2EEOff) { - const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - - if (isWorkspaceE2EE) - throw BadRequestError({ - message: "Failed workspace authorization due to end-to-end encryption not being disabled" - }); - } + if (!workspace) throw WorkspaceNotFoundError({ + message: "Failed to find workspace" + }); let membership; switch (authData.actor.type) { @@ -84,34 +50,6 @@ export const validateClientForWorkspace = async ({ return { membership, workspace }; case ActorType.SERVICE: - if (checkIPAllowlist) { - const trustedIps = await TrustedIP.find({ - workspace: workspaceId - }); - - if (trustedIps.length > 0) { - // case: check the IP address of the inbound request against trusted IPs - - const blockList = new net.BlockList(); - - for (const trustedIp of trustedIps) { - if (trustedIp.prefix !== undefined) { - blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type); - } else { - blockList.addAddress(trustedIp.ipAddress, trustedIp.type); - } - } - - const { type } = extractIPDetails(authData.ipAddress); - const check = blockList.check(authData.ipAddress, type); - - if (!check) - throw UnauthorizedRequestError({ - message: "Failed workspace authorization" - }); - } - } - await validateServiceTokenDataClientForWorkspace({ serviceTokenData: authData.authPayload as IServiceTokenData, workspaceId,