diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 14c717e38..840c2d504 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -17,6 +17,8 @@ import { getJwtRefreshSecret } from "../../config"; import { ActorType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -32,7 +34,9 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; + const { + body: { email, clientPublicKey } + } = await validateRequest(reqValidator.Login1V1, req); const user = await User.findOne({ email @@ -76,7 +80,10 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - const { email, clientProof } = req.body; + const { + body: { email, clientProof } + } = await validateRequest(reqValidator.Login2V1, req); + const user = await User.findOne({ email }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); @@ -194,6 +201,14 @@ export const logout = async (req: Request, res: Response) => { }); }; +export const getCommonPasswords = async (req: Request, res: Response) => { + const commonPasswords = fs + .readFileSync(path.resolve(__dirname, "../../data/" + "common_passwords.txt"), "utf8") + .split("\n"); + + return res.status(200).send(commonPasswords); +}; + export const revokeAllSessions = async (req: Request, res: Response) => { await TokenVersion.updateMany( { diff --git a/backend/src/controllers/v1/botController.ts b/backend/src/controllers/v1/botController.ts index b2e757541..2f70f25cf 100644 --- a/backend/src/controllers/v1/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -2,10 +2,19 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { Bot, BotKey } from "../../models"; import { createBot } from "../../helpers/bot"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/bot"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { BadRequestError } from "../../utils/errors"; interface BotKey { - encryptedKey: string; - nonce: string; + encryptedKey: string; + nonce: string; } /** @@ -16,23 +25,30 @@ interface BotKey { * @returns */ export const getBotByWorkspaceId = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); let bot = await Bot.findOne({ - workspace: workspaceId, + workspace: workspaceId }); - + if (!bot) { - // case: bot doesn't exist for workspace with id [workspaceId] - // -> create a new bot and return it - bot = await createBot({ - name: "Infisical Bot", - workspaceId: new Types.ObjectId(workspaceId), - }); + // case: bot doesn't exist for workspace with id [workspaceId] + // -> create a new bot and return it + bot = await createBot({ + name: "Infisical Bot", + workspaceId: new Types.ObjectId(workspaceId) + }); } - + return res.status(200).send({ - bot, + bot }); }; @@ -43,46 +59,69 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => { * @returns */ export const setBotActiveState = async (req: Request, res: Response) => { - const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; - - if (isActive) { - // bot state set to active -> share workspace key with bot - if (!botKey?.encryptedKey || !botKey?.nonce) { - return res.status(400).send({ - message: "Failed to set bot state to active - missing bot key", - }); - } - - await BotKey.findOneAndUpdate({ - workspace: req.bot.workspace, - }, { - encryptedKey: botKey.encryptedKey, - nonce: botKey.nonce, - sender: req.user._id, - bot: req.bot._id, - workspace: req.bot.workspace, - }, { - upsert: true, - new: true, - }); - } else { - // case: bot state set to inactive -> delete bot's workspace key - await BotKey.deleteOne({ - bot: req.bot._id, - }); + const { + body: { botKey, isActive }, + params: { botId } + } = await validateRequest(reqValidator.SetBotActiveStateV1, req); + + const bot = await Bot.findById(botId); + if (!bot) { + throw BadRequestError({ message: "Bot not found" }); + } + const userId = req.user._id; + + const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Integrations + ); + + if (isActive) { + // bot state set to active -> share workspace key with bot + if (!botKey?.encryptedKey || !botKey?.nonce) { + return res.status(400).send({ + message: "Failed to set bot state to active - missing bot key" + }); } - const bot = await Bot.findOneAndUpdate({ - _id: req.bot._id, - }, { - isActive, - }, { - new: true, - }); - - if (!bot) throw new Error("Failed to update bot active state"); - - return res.status(200).send({ - bot, + await BotKey.findOneAndUpdate( + { + workspace: bot.workspace + }, + { + encryptedKey: botKey.encryptedKey, + nonce: botKey.nonce, + sender: userId, + bot: bot._id, + workspace: bot.workspace + }, + { + upsert: true, + new: true + } + ); + } else { + // case: bot state set to inactive -> delete bot's workspace key + await BotKey.deleteOne({ + bot: bot._id }); + } + + const updatedBot = await Bot.findOneAndUpdate( + { + _id: bot._id + }, + { + isActive + }, + { + new: true + } + ); + + if (!updatedBot) throw new Error("Failed to update bot active state"); + + return res.status(200).send({ + bot + }); }; diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index a32bedf72..c468444a3 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { standardRequest } from "../../config/request"; import { getApps, getTeams, revokeAccess } from "../../integrations"; -import { Bot, IntegrationAuth } from "../../models"; +import { Bot, IntegrationAuth, Workspace } from "../../models"; import { EventType } from "../../ee/models"; import { IntegrationService } from "../../services"; import { EEAuditLogService } from "../../ee/services"; @@ -18,12 +18,25 @@ import { getIntegrationOptions as getIntegrationOptionsFunc } from "../../variables"; import { exchangeRefresh } from "../../integrations"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/integrationAuth"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { getIntegrationAuthAccessHelper } from "../../helpers"; +import { ObjectId } from "mongodb"; /*** * Return integration authorization with id [integrationAuthId] */ export const getIntegrationAuth = async (req: Request, res: Response) => { - const { integrationAuthId } = req.params; + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthV1, req); + const integrationAuth = await IntegrationAuth.findById(integrationAuthId); if (!integrationAuth) @@ -31,6 +44,15 @@ export const getIntegrationAuth = async (req: Request, res: Response) => { message: "Failed to find integration authorization" }); + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + return res.status(200).send({ integrationAuth }); @@ -51,15 +73,19 @@ export const getIntegrationOptions = async (req: Request, res: Response) => { * @returns */ export const oAuthExchange = async (req: Request, res: Response) => { - const { - workspaceId, - code, - integration, - url - } = req.body; + const { + body: { integration, workspaceId, code, url } + } = await validateRequest(reqValidator.OauthExchangeV1, req); if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration"); - const environments = req.membership.workspace?.environments || []; + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); + + const workspace = await Workspace.findById(workspaceId); + const environments = workspace?.environments || []; if (environments.length === 0) { throw new Error("Failed to get environments"); } @@ -99,25 +125,16 @@ export const oAuthExchange = async (req: Request, res: Response) => { export const saveIntegrationToken = async (req: Request, res: Response) => { // TODO: refactor // TODO: check if access token is valid for each integration - let integrationAuth; const { - workspaceId, - accessId, - refreshToken, - accessToken, - url, - namespace, - integration - }: { - workspaceId: string; - accessId: string | undefined; - refreshToken: string | undefined; - accessToken: string | undefined; - url: string; - namespace: string; - integration: string; - } = req.body; + body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken } + } = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); const bot = await Bot.findOne({ workspace: new Types.ObjectId(workspaceId), @@ -169,7 +186,7 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { } if (!integrationAuth) throw new Error("Failed to save integration access token"); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -195,13 +212,29 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { * @returns */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { - const teamId = req.query.teamId as string; - const workspaceSlug = req.query.workspaceSlug as string; + const { + params: { integrationAuthId }, + query: { teamId, workspaceSlug } + } = await validateRequest(reqValidator.GetIntegrationAuthAppsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken, accessId } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); const apps = await getApps({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken, - accessId: req.accessId, + integrationAuth: integrationAuth, + accessToken: accessToken, + accessId: accessId, ...(teamId && { teamId }), ...(workspaceSlug && { workspaceSlug }) }); @@ -218,9 +251,27 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthTeamsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + const teams = await getTeams({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken + integrationAuth: integrationAuth, + accessToken: accessToken }); return res.status(200).send({ @@ -235,7 +286,24 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { * @param res */ export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthVercelBranchesV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface VercelBranch { ref: string; @@ -245,9 +313,9 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon const params = new URLSearchParams({ projectId: appId, - ...(req.integrationAuth.teamId + ...(integrationAuth.teamId ? { - teamId: req.integrationAuth.teamId + teamId: integrationAuth.teamId } : {}) }); @@ -260,7 +328,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon { params, headers: { - Authorization: `Bearer ${req.accessToken}`, + Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" } } @@ -281,7 +349,24 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon * @param res */ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthRailwayEnvironmentsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface RailwayEnvironment { node: { @@ -331,7 +416,7 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R }, { headers: { - Authorization: `Bearer ${req.accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } } @@ -357,7 +442,24 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R * @param res */ export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthRailwayServicesV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface RailwayService { node: { @@ -422,7 +524,7 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo }, { headers: { - Authorization: `Bearer ${req.accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } } @@ -446,7 +548,6 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo * @returns */ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => { - interface WorkspaceResponse { size: number; page: number; @@ -466,31 +567,46 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R updated_on: string; } + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthBitbucketWorkspacesV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + const workspaces: Workspace[] = []; let hasNextPage = true; - let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces` + let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`; while (hasNextPage) { - const { data }: { data: WorkspaceResponse } = await standardRequest.get( - workspaceUrl, - { - headers: { - Authorization: `Bearer ${req.accessToken}`, - "Accept-Encoding": "application/json" - } + const { data }: { data: WorkspaceResponse } = await standardRequest.get(workspaceUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); - + }); + if (data?.values.length > 0) { data.values.forEach((workspace) => { - workspaces.push(workspace) - }) + workspaces.push(workspace); + }); } if (data.next) { - workspaceUrl = data.next + workspaceUrl = data.next; } else { - hasNextPage = false + hasNextPage = false; } } @@ -501,13 +617,30 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R /** * Return list of secret groups for Northflank project with id [appId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res: Response) => { - const appId = req.query.appId as string; - + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthNorthflankSecretGroupsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + interface NorthflankSecretGroup { id: string; name: string; @@ -515,43 +648,41 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res priority: number; projectId: string; } - + interface SecretGroup { name: string; groupId: string; } - + const secretGroups: SecretGroup[] = []; - if (appId && appId !== "") { + if (appId && appId !== "") { let page = 1; const perPage = 10; let hasMorePages = true; - - while(hasMorePages) { + + while (hasMorePages) { const params = new URLSearchParams({ page: String(page), per_page: String(perPage), - filter: "all", + filter: "all" }); const { data: { - data: { - secrets - } + data: { secrets } } - } = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] }}>( + } = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] } }>( `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${appId}/secrets`, { params, headers: { - Authorization: `Bearer ${req.accessToken}`, - "Accept-Encoding": "application/json", - }, + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } ); - + secrets.forEach((a: any) => { secretGroups.push({ name: a.name, @@ -566,11 +697,11 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res page++; } } - + return res.status(200).send({ secretGroups }); -} +}; /** * Return list of build configs for TeamCity project with id [appId] @@ -579,7 +710,23 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res * @returns */ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId,appId }, + } = await validateRequest(reqValidator.GetIntegrationAuthTeamCityBuildConfigsV1 , req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface TeamCityBuildConfig { id: string; @@ -630,30 +777,48 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: * @returns */ export const deleteIntegrationAuth = async (req: Request, res: Response) => { - const integrationAuth = await revokeAccess({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.DeleteIntegrationAuthV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) }); - - if (!integrationAuth) return res.status(400).send({ - message: "Failed to find integration authorization" + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Integrations + ); + + const deletedIntegrationAuth = await revokeAccess({ + integrationAuth: integrationAuth, + accessToken: accessToken }); + if (!deletedIntegrationAuth) + return res.status(400).send({ + message: "Failed to find integration authorization" + }); + await EEAuditLogService.createAuditLog( req.authData, { type: EventType.UNAUTHORIZE_INTEGRATION, metadata: { - integration: integrationAuth.integration + integration: deletedIntegrationAuth.integration } }, { - workspaceId: integrationAuth.workspace + workspaceId: deletedIntegrationAuth.workspace } ); return res.status(200).send({ - integrationAuth + integrationAuth: deletedIntegrationAuth }); }; - diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 84d00f6bb..c9d4180b0 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Folder, Integration } from "../../models"; +import { IWorkspace, Integration, Folder, IntegrationAuth } from "../../models"; import { EventService } from "../../services"; import { eventStartIntegration } from "../../events"; import { getFolderByPath } from "../../services/FolderService"; @@ -8,6 +8,14 @@ import { BadRequestError } from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/integration"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Create/initialize an (empty) integration for integration authorization @@ -17,24 +25,42 @@ import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/ */ export const createIntegration = async (req: Request, res: Response) => { const { - integrationAuthId, - app, - appId, - isActive, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region, - secretPath, - metadata - } = req.body; + body: { + isActive, + sourceEnvironment, + secretPath, + app, + path, + appId, + owner, + region, + targetService, + targetServiceId, + integrationAuthId, + targetEnvironment, + targetEnvironmentId + } + } = await validateRequest(reqValidator.CreateIntegrationV1, req); + + const integrationAuth = await IntegrationAuth.findById(integrationAuthId) + .populate<{ workspace: IWorkspace }>("workspace") + .select( + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + ); + + if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); const folders = await Folder.findOne({ - workspace: req.integrationAuth.workspace._id, + workspace: integrationAuth.workspace._id, environment: sourceEnvironment }); @@ -42,7 +68,7 @@ export const createIntegration = async (req: Request, res: Response) => { const folder = getFolderByPath(folders.nodes, secretPath); if (!folder) { throw BadRequestError({ - message: "Path for service token does not exist" + message: "Folder path doesn't exist" }); } } @@ -51,7 +77,7 @@ export const createIntegration = async (req: Request, res: Response) => { // initialize new integration after saving integration access token const integration = await new Integration({ - workspace: req.integrationAuth.workspace._id, + workspace: integrationAuth.workspace._id, environment: sourceEnvironment, isActive, app, @@ -64,9 +90,8 @@ export const createIntegration = async (req: Request, res: Response) => { path, region, secretPath, - integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId), - metadata + integration: integrationAuth.integration, + integrationAuth: new Types.ObjectId(integrationAuthId) }).save(); if (integration) { @@ -120,17 +145,32 @@ export const updateIntegration = async (req: Request, res: Response) => { // integration has the correct fields populated in [Integration] const { - environment, - isActive, - app, - appId, - targetEnvironment, - owner, // github-specific integration param - secretPath - } = req.body; + body: { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, // github-specific integration param + secretPath + }, + params: { integrationId } + } = await validateRequest(reqValidator.UpdateIntegrationV1, req); + + const integration = await Integration.findById(integrationId); + if (!integration) throw BadRequestError({ message: "Integration not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integration.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Integrations + ); const folders = await Folder.findOne({ - workspace: req.integration.workspace, + workspace: integration.workspace, environment }); @@ -143,9 +183,9 @@ export const updateIntegration = async (req: Request, res: Response) => { } } - const integration = await Integration.findOneAndUpdate( + const updatedIntegration = await Integration.findOneAndUpdate( { - _id: req.integration._id + _id: integration._id }, { environment, @@ -161,18 +201,18 @@ export const updateIntegration = async (req: Request, res: Response) => { } ); - if (integration) { + if (updatedIntegration) { // trigger event - push secrets EventService.handleEvent({ event: eventStartIntegration({ - workspaceId: integration.workspace, + workspaceId: updatedIntegration.workspace, environment }) }); } return res.status(200).send({ - integration + integration: updatedIntegration }); }; @@ -183,13 +223,27 @@ export const updateIntegration = async (req: Request, res: Response) => { * @returns */ export const deleteIntegration = async (req: Request, res: Response) => { - const { integrationId } = req.params; + const { + params: { integrationId } + } = await validateRequest(reqValidator.DeleteIntegrationV1, req); - const integration = await Integration.findOneAndDelete({ + const integration = await Integration.findById(integrationId); + if (!integration) throw BadRequestError({ message: "Integration not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integration.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Integrations + ); + + const deletedIntegration = await Integration.findOneAndDelete({ _id: integrationId }); - if (!integration) throw new Error("Failed to find integration"); + if (!deletedIntegration) throw new Error("Failed to find integration"); await EEAuditLogService.createAuditLog( req.authData, diff --git a/backend/src/controllers/v1/keyController.ts b/backend/src/controllers/v1/keyController.ts index caf4fbf40..87afa936d 100644 --- a/backend/src/controllers/v1/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -4,6 +4,14 @@ import { Key } from "../../models"; import { findMembership } from "../../helpers/membership"; import { EventType } from "../../ee/models"; import { EEAuditLogService } from "../../ee/services"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/key"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with @@ -13,13 +21,21 @@ import { EEAuditLogService } from "../../ee/services"; * @returns */ export const uploadKey = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { key } = req.body; + const { + params: { workspaceId }, + body: { key } + } = await validateRequest(reqValidator.UploadKeyV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); // validate membership of receiver const receiverMembership = await findMembership({ user: key.userId, - workspace: workspaceId, + workspace: workspaceId }); if (!receiverMembership) { @@ -31,12 +47,12 @@ export const uploadKey = async (req: Request, res: Response) => { nonce: key.nonce, sender: req.user._id, receiver: key.userId, - workspace: workspaceId, + workspace: workspaceId }).save(); - return res.status(200).send({ - message: "Successfully uploaded key to workspace", - }); + return res.status(200).send({ + message: "Successfully uploaded key to workspace" + }); }; /** @@ -46,21 +62,23 @@ export const uploadKey = async (req: Request, res: Response) => { * @returns */ export const getLatestKey = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetLatestKeyV1, req); + // get latest key const latestKey = await Key.find({ workspace: workspaceId, - receiver: req.user._id, + receiver: req.user._id }) .sort({ createdAt: -1 }) .limit(1) .populate("sender", "+publicKey"); - const resObj: any = {}; + const resObj: any = {}; - if (latestKey.length > 0) { - resObj["latestKey"] = latestKey[0]; + if (latestKey.length > 0) { + resObj["latestKey"] = latestKey[0]; await EEAuditLogService.createAuditLog( req.authData, { @@ -73,7 +91,7 @@ export const getLatestKey = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId) } ); - } + } - return res.status(200).send(resObj); + return res.status(200).send(resObj); }; diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 885f16690..2c63525a2 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -7,6 +7,14 @@ import { sendMail } from "../../helpers/nodemailer"; import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; import { getSiteURL } from "../../config"; import { EEAuditLogService } from "../../ee/services"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/membership"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Check that user is a member of workspace with id [workspaceId] @@ -15,7 +23,10 @@ import { EEAuditLogService } from "../../ee/services"; * @returns */ export const validateMembership = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.ValidateMembershipV1, req); + // validate membership const membership = await findMembership({ user: req.user._id, @@ -38,8 +49,10 @@ export const validateMembership = async (req: Request, res: Response) => { * @returns */ export const deleteMembership = async (req: Request, res: Response) => { - const { membershipId } = req.params; - + const { + params: { membershipId } + } = await validateRequest(reqValidator.DeleteMembershipV1, req); + // check if membership to delete exists const membershipToDelete = await Membership.findOne({ _id: membershipId @@ -49,27 +62,20 @@ export const deleteMembership = async (req: Request, res: Response) => { throw new Error("Failed to delete workspace membership that doesn't exist"); } - // check if user is a member and admin of the workspace - // whose membership we wish to delete - const membership = await Membership.findOne({ - user: req.user._id, - workspace: membershipToDelete.workspace - }); - - if (!membership) { - throw new Error("Failed to validate workspace membership"); - } - - if (membership.role !== ADMIN) { - // user is not an admin member of the workspace - throw new Error("Insufficient role for deleting workspace membership"); - } + const { permission } = await getUserProjectPermissions( + req.user._id, + membershipToDelete.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Member + ); // delete workspace membership const deletedMembership = await deleteMember({ membershipId: membershipToDelete._id.toString() }); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -80,7 +86,7 @@ export const deleteMembership = async (req: Request, res: Response) => { } }, { - workspaceId: membership.workspace + workspaceId: membershipToDelete.workspace } ); @@ -96,43 +102,37 @@ export const deleteMembership = async (req: Request, res: Response) => { * @returns */ export const changeMembershipRole = async (req: Request, res: Response) => { - const { membershipId } = req.params; - const { role } = req.body; + const { + body: { role }, + params: { membershipId } + } = await validateRequest(reqValidator.ChangeMembershipRoleV1, req); if (![ADMIN, MEMBER].includes(role)) { throw new Error("Failed to validate role"); } // validate target membership - const membershipToChangeRole = await Membership - .findById(membershipId) - .populate<{ user: IUser }>("user"); + const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>( + "user" + ); if (!membershipToChangeRole) { throw new Error("Failed to find membership to change role"); } - // check if user is a member and admin of target membership's - // workspace - const membership = await findMembership({ - user: req.user._id, - workspace: membershipToChangeRole.workspace - }); + const { permission } = await getUserProjectPermissions( + req.user._id, + membershipToChangeRole.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); - if (!membership) { - throw new Error("Failed to validate membership"); - } - - if (membership.role !== ADMIN) { - // user is not an admin member of the workspace - throw new Error("Insufficient role for changing member roles"); - } - const oldRole = membershipToChangeRole.role; - membershipToChangeRole.role = role; await membershipToChangeRole.save(); - + await EEAuditLogService.createAuditLog( req.authData, { diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 22b90a61a..65447bcf8 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -14,6 +14,8 @@ import { getSiteURL } from "../../config"; import { ActorType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; /** * Password reset step 1: Send email verification link to email [email] @@ -23,7 +25,9 @@ import { ActorType } from "../../ee/models"; * @returns */ export const emailPasswordReset = async (req: Request, res: Response) => { - const email: string = req.body.email; + const { + body: { email } + } = await validateRequest(reqValidator.EmailPasswordResetV1, req); const user = await User.findOne({ email }).select("+publicKey"); if (!user || !user?.publicKey) { @@ -62,7 +66,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => { * @returns */ export const emailPasswordResetVerify = async (req: Request, res: Response) => { - const { email, code } = req.body; + const { + body: { email, code } + } = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req); const user = await User.findOne({ email }).select("+publicKey"); if (!user || !user?.publicKey) { @@ -103,8 +109,10 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { */ export const srp1 = async (req: Request, res: Response) => { // return salt, serverPublicKey as part of first step of SRP protocol + const { + body: { clientPublicKey } + } = await validateRequest(reqValidator.Srp1V1, req); - const { clientPublicKey } = req.body; const user = await User.findOne({ email: req.user.email }).select("+salt +verifier"); @@ -149,16 +157,18 @@ export const srp1 = async (req: Request, res: Response) => { */ export const changePassword = async (req: Request, res: Response) => { const { - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - } = req.body; + body: { + clientProof, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + } + } = await validateRequest(reqValidator.ChangePasswordV1, req); const user = await User.findOne({ email: req.user.email @@ -208,10 +218,7 @@ export const changePassword = async (req: Request, res: Response) => { } ); - if ( - req.authData.actor.type === ActorType.USER && - req.authData.tokenVersionId - ) { + if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { await clearTokens(req.authData.tokenVersionId); } @@ -246,8 +253,9 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { // create/change backup private key // requires verifying [clientProof] as part of second step of SRP protocol // as initiated in /srp1 - - const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body; + const { + body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag } + } = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req); const user = await User.findOne({ email: req.user.email }).select("+salt +verifier"); @@ -325,15 +333,17 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => { const { - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - } = req.body; + body: { + encryptedPrivateKey, + protectedKeyTag, + protectedKey, + protectedKeyIV, + salt, + verifier, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag + } + } = await validateRequest(reqValidator.ResetPasswordV1, req); await User.findByIdAndUpdate( req.user._id.toString(), diff --git a/backend/src/controllers/v1/secretImportController.ts b/backend/src/controllers/v1/secretImportController.ts index 23be98096..209da36ac 100644 --- a/backend/src/controllers/v1/secretImportController.ts +++ b/backend/src/controllers/v1/secretImportController.ts @@ -1,15 +1,28 @@ import { Request, Response } from "express"; -import { isValidScope, validateMembership } from "../../helpers"; +import { isValidScope } from "../../helpers"; import { Folder, SecretImport, ServiceTokenData } from "../../models"; import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getFolderWithPathFromId } from "../../services/FolderService"; -import { BadRequestError, ResourceNotFoundError,UnauthorizedRequestError } from "../../utils/errors"; -import { ADMIN, MEMBER } from "../../variables"; +import { + BadRequestError, + ResourceNotFoundError, + UnauthorizedRequestError +} from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/secretImports"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError, subject } from "@casl/ability"; export const createSecretImport = async (req: Request, res: Response) => { - const { workspaceId, environment, folderId, secretImport } = req.body; + const { + body: { workspaceId, environment, folderId, secretImport } + } = await validateRequest(reqValidator.CreateSecretImportV1, req); const folders = await Folder.findOne({ workspace: workspaceId, @@ -27,12 +40,19 @@ export const createSecretImport = async (req: Request, res: Response) => { const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId); secretPath = folderPath; } + if (req.authData.authPayload instanceof ServiceTokenData) { // root check const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); } const importSecDoc = await SecretImport.findOne({ @@ -40,8 +60,10 @@ export const createSecretImport = async (req: Request, res: Response) => { environment, folderId }); - - const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, folderId).folderPath:"/"; + + const importToSecretPath = folders + ? getFolderWithPathFromId(folders.nodes, folderId).folderPath + : "/"; if (!importSecDoc) { const doc = new SecretImport({ @@ -50,7 +72,7 @@ export const createSecretImport = async (req: Request, res: Response) => { folderId, imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }] }); - + await doc.save(); await EEAuditLogService.createAuditLog( req.authData, @@ -84,7 +106,7 @@ export const createSecretImport = async (req: Request, res: Response) => { secretPath: secretImport.secretPath }); await importSecDoc.save(); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -108,32 +130,30 @@ export const createSecretImport = async (req: Request, res: Response) => { // to keep the ordering, you must pass all the imports in here not the only updated one // this is because the order decide which import gets overriden export const updateSecretImport = async (req: Request, res: Response) => { - const { id } = req.params; - const { secretImports } = req.body; + const { + body: { secretImports }, + params: { id } + } = await validateRequest(reqValidator.UpdateSecretImportV1, req); + const importSecDoc = await SecretImport.findById(id); if (!importSecDoc) { throw BadRequestError({ message: "Import not found" }); } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: importSecDoc.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); - } else { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + if (req.authData.authPayload instanceof ServiceTokenData) { + // token permission check const isValidScopeAccess = isValidScope( req.authData.authPayload, importSecDoc.environment, @@ -142,23 +162,25 @@ export const updateSecretImport = async (req: Request, res: Response) => { if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + // non token entry check + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); } const orderBefore = importSecDoc.imports; importSecDoc.imports = secretImports; - + await importSecDoc.save(); - - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment, - }).lean(); - - if (!folders) throw ResourceNotFoundError({ - message: "Failed to find folder" - }); - - const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/"; await EEAuditLogService.createAuditLog( req.authData, @@ -166,7 +188,7 @@ export const updateSecretImport = async (req: Request, res: Response) => { type: EventType.UPDATE_SECRET_IMPORT, metadata: { importToEnvironment: importSecDoc.environment, - importToSecretPath, + importToSecretPath: secretPath, secretImportId: importSecDoc._id.toString(), folderId: importSecDoc.folderId.toString(), orderBefore, @@ -181,32 +203,29 @@ export const updateSecretImport = async (req: Request, res: Response) => { }; export const deleteSecretImport = async (req: Request, res: Response) => { - const { id } = req.params; - const { secretImportEnv, secretImportPath } = req.body; + const { + params: { id }, + body: { secretImportEnv, secretImportPath } + } = await validateRequest(reqValidator.DeleteSecretImportV1, req); + const importSecDoc = await SecretImport.findById(id); if (!importSecDoc) { throw BadRequestError({ message: "Import not found" }); } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: importSecDoc.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); - } else { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + if (req.authData.authPayload instanceof ServiceTokenData) { const isValidScopeAccess = isValidScope( req.authData.authPayload, importSecDoc.environment, @@ -215,6 +234,18 @@ export const deleteSecretImport = async (req: Request, res: Response) => { if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); } importSecDoc.imports = importSecDoc.imports.filter( ({ environment, secretPath }) => @@ -222,17 +253,6 @@ export const deleteSecretImport = async (req: Request, res: Response) => { ); await importSecDoc.save(); - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment, - }).lean(); - - if (!folders) throw ResourceNotFoundError({ - message: "Failed to find folder" - }); - - const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/"; - await EEAuditLogService.createAuditLog( req.authData, { @@ -243,7 +263,7 @@ export const deleteSecretImport = async (req: Request, res: Response) => { importFromEnvironment: secretImportEnv, importFromSecretPath: secretImportPath, importToEnvironment: importSecDoc.environment, - importToSecretPath + importToSecretPath: secretPath } }, { @@ -266,19 +286,19 @@ export const getSecretImports = async (req: Request, res: Response) => { return res.status(200).json({ secretImport: {} }); } + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + if (req.authData.authPayload instanceof ServiceTokenData) { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } - const isValidScopeAccess = isValidScope( req.authData.authPayload, importSecDoc.environment, @@ -287,6 +307,18 @@ export const getSecretImports = async (req: Request, res: Response) => { if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); } return res.status(200).json({ secretImport: importSecDoc }); @@ -308,19 +340,19 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => { return res.status(200).json({ secrets: [] }); } + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + if (req.authData.authPayload instanceof ServiceTokenData) { // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } - const isValidScopeAccess = isValidScope( req.authData.authPayload, importSecDoc.environment, @@ -329,6 +361,25 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => { if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: importSecDoc.environment, + secretPath + }) + ); } await EEAuditLogService.createAuditLog( diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index cbc642592..5f9595d88 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -1,10 +1,12 @@ +import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; import { Types } from "mongoose"; import { EventType, FolderVersion } from "../../ee/models"; import { EEAuditLogService, EESecretService } from "../../ee/services"; -import { validateMembership } from "../../helpers/membership"; import { isValidScope } from "../../helpers/secrets"; -import { Folder, Secret, ServiceTokenData } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import { Secret, ServiceTokenData } from "../../models"; +import {Folder} from "../../models/folder"; import { appendFolder, deleteFolderById, @@ -15,12 +17,20 @@ import { getParentFromFolderId, validateFolderName } from "../../services/FolderService"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; -import { ADMIN, MEMBER } from "../../variables"; +import * as reqValidator from "../../validation/folders"; // verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { - const { workspaceId, environment, folderName, parentFolderId } = req.body; + const { + body: { workspaceId, environment, folderName, parentFolderId } + } = await validateRequest(reqValidator.CreateFolderV1, req); + if (!validateFolderName(folderName)) { throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" @@ -32,8 +42,20 @@ export const createFolder = async (req: Request, res: Response) => { environment }).lean(); + if (req.user) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const secretPath = + folders && parentFolderId + ? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath + : "/"; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); + } + // space has no folders initialized - + if (!folders) { if (req.authData.authPayload instanceof ServiceTokenData) { // root check @@ -62,10 +84,10 @@ export const createFolder = async (req: Request, res: Response) => { }); await folderVersion.save(); await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment }); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -86,9 +108,9 @@ export const createFolder = async (req: Request, res: Response) => { } const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); - + await Folder.findByIdAndUpdate(folders._id, folders); - + const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId( folders.nodes, parentFolderId || "root" @@ -116,13 +138,13 @@ export const createFolder = async (req: Request, res: Response) => { await folderVersion.save(); await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, folderId: parentFolderId }); - - const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id); - + + const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id); + await EEAuditLogService.createAuditLog( req.authData, { @@ -143,8 +165,11 @@ export const createFolder = async (req: Request, res: Response) => { }; export const updateFolderById = async (req: Request, res: Response) => { - const { folderId } = req.params; - const { name, workspaceId, environment } = req.body; + const { + body: { workspaceId, environment, name }, + params: { folderId } + } = await validateRequest(reqValidator.UpdateFolderV1, req); + if (!validateFolderName(name)) { throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" @@ -156,21 +181,21 @@ export const updateFolderById = async (req: Request, res: Response) => { throw BadRequestError({ message: "The folder doesn't exist" }); } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER] - }); - } - const parentFolder = getParentFromFolderId(folders.nodes, folderId); if (!parentFolder) { throw BadRequestError({ message: "The folder doesn't exist" }); } + + if (req.user) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); + } + const folder = parentFolder.children.find(({ id }) => id === folderId); - if (!folder) { throw BadRequestError({ message: "The folder doesn't exist" }); } @@ -197,13 +222,13 @@ export const updateFolderById = async (req: Request, res: Response) => { await folderVersion.save(); await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, folderId: parentFolder.id }); - const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id); - + const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id); + await EEAuditLogService.createAuditLog( req.authData, { @@ -228,37 +253,35 @@ export const updateFolderById = async (req: Request, res: Response) => { }; export const deleteFolder = async (req: Request, res: Response) => { - const { folderId } = req.params; - const { workspaceId, environment } = req.body; + const { + params: { folderId }, + body: { environment, workspaceId } + } = await validateRequest(reqValidator.DeleteFolderV1, req); const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { throw BadRequestError({ message: "The folder doesn't exist" }); } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER] - }); - } - - const {folderPath} = getFolderWithPathFromId(folders.nodes, folderId); - const delOp = deleteFolderById(folders.nodes, folderId); if (!delOp) { throw BadRequestError({ message: "The folder doesn't exist" }); } const { deletedNode: delFolder, parent: parentFolder } = delOp; + const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id); if (req.authData.authPayload instanceof ServiceTokenData) { - const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + // check that user is a member of the workspace + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); } parentFolder.version += 1; @@ -280,7 +303,7 @@ export const deleteFolder = async (req: Request, res: Response) => { } await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, folderId: parentFolder.id }); @@ -288,12 +311,12 @@ export const deleteFolder = async (req: Request, res: Response) => { await EEAuditLogService.createAuditLog( req.authData, { - type: EventType.DELETE_FOLDER , + type: EventType.DELETE_FOLDER, metadata: { environment, folderId, folderName: delFolder.name, - folderPath + folderPath: secretPath } }, { @@ -306,28 +329,30 @@ export const deleteFolder = async (req: Request, res: Response) => { // TODO: validate workspace export const getFolders = async (req: Request, res: Response) => { - const { workspaceId, environment, parentFolderId, parentFolderPath } = req.query as { - workspaceId: string; - environment: string; - parentFolderId?: string; - parentFolderPath?: string; - }; + const { + query: { workspaceId, environment, parentFolderId, parentFolderPath } + } = await validateRequest(reqValidator.GetFoldersV1, req); const folders = await Folder.findOne({ workspace: workspaceId, environment }); + + if (req.user) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const secretPath = + folders && parentFolderId + ? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath + : parentFolderPath || "/"; + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); + } + if (!folders) { res.send({ folders: [], dir: [] }); return; } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER] - }); - } - // if instead of parentFolderId given a path like /folder1/folder2 if (parentFolderPath) { if (req.authData.authPayload instanceof ServiceTokenData) { diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index ce464e214..68a4e07f2 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -10,6 +10,8 @@ import { getSmtpConfigured } from "../../config"; import { validateUserEmail } from "../../validation"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -19,7 +21,9 @@ import { validateUserEmail } from "../../validation"; * @returns */ export const beginEmailSignup = async (req: Request, res: Response) => { - const email: string = req.body.email; + const { + body: { email } + } = await validateRequest(reqValidator.BeginEmailSignUpV1, req); // validate that email is not disposable validateUserEmail(email); @@ -50,7 +54,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => { */ export const verifyEmailSignup = async (req: Request, res: Response) => { let user; - const { email, code } = req.body; + const { + body: { email, code } + } = await validateRequest(reqValidator.VerifyEmailSignUpV1, req); // initialize user account user = await User.findOne({ email }).select("+publicKey"); diff --git a/backend/src/controllers/v1/userActionController.ts b/backend/src/controllers/v1/userActionController.ts index 9e7354ce1..9a151f8dd 100644 --- a/backend/src/controllers/v1/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -1,5 +1,7 @@ import { Request, Response } from "express"; +import { validateRequest } from "../../helpers/validation"; import { UserAction } from "../../models"; +import * as reqValidator from "../../validation/action"; /** * Add user action [action] @@ -8,26 +10,27 @@ import { UserAction } from "../../models"; * @returns */ export const addUserAction = async (req: Request, res: Response) => { - // add/record new action [action] for user with id [req.user._id] - - const { action } = req.body; + // add/record new action [action] for user with id [req.user._id] + const { + body: { action } + } = await validateRequest(reqValidator.AddUserActionV1, req); const userAction = await UserAction.findOneAndUpdate( { user: req.user._id, - action, + action }, { user: req.user._id, action }, { new: true, - upsert: true, + upsert: true } ); - return res.status(200).send({ - message: "Successfully recorded user action", - userAction, - }); + return res.status(200).send({ + message: "Successfully recorded user action", + userAction + }); }; /** @@ -37,15 +40,17 @@ export const addUserAction = async (req: Request, res: Response) => { * @returns */ export const getUserAction = async (req: Request, res: Response) => { - // get user action [action] for user with id [req.user._id] - const action: string = req.query.action as string; + // get user action [action] for user with id [req.user._id] + const { + query: { action } + } = await validateRequest(reqValidator.GetUserActionV1, req); const userAction = await UserAction.findOne({ user: req.user._id, - action, + action }); - return res.status(200).send({ - userAction, - }); + return res.status(200).send({ + userAction + }); }; diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts index 7e84aae87..e4cb1b825 100644 --- a/backend/src/controllers/v1/webhookController.ts +++ b/backend/src/controllers/v1/webhookController.ts @@ -1,16 +1,32 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { client, getRootEncryptionKey } from "../../config"; -import { validateMembership } from "../../helpers"; import { Webhook } from "../../models"; import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; -import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables"; +import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64 } from "../../variables"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/webhooks"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; export const createWebhook = async (req: Request, res: Response) => { - const { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } = req.body; + const { + body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } + } = await validateRequest(reqValidator.CreateWebhookV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Webhooks + ); + const webhook = new Webhook({ workspace: workspaceId, environment, @@ -29,7 +45,7 @@ export const createWebhook = async (req: Request, res: Response) => { } await webhook.save(); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -43,7 +59,7 @@ export const createWebhook = async (req: Request, res: Response) => { } }, { - workspaceId + workspaceId: new Types.ObjectId(workspaceId) } ); @@ -54,19 +70,24 @@ export const createWebhook = async (req: Request, res: Response) => { }; export const updateWebhook = async (req: Request, res: Response) => { - const { webhookId } = req.params; - const { isDisabled } = req.body; + const { + body: { isDisabled }, + params: { webhookId } + } = await validateRequest(reqValidator.UpdateWebhookV1, req); + const webhook = await Webhook.findById(webhookId); if (!webhook) { throw BadRequestError({ message: "Webhook not found!!" }); } - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: webhook.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); + const { permission } = await getUserProjectPermissions( + req.user._id, + webhook.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Webhooks + ); if (typeof isDisabled !== undefined) { webhook.isDisabled = isDisabled; @@ -97,19 +118,24 @@ export const updateWebhook = async (req: Request, res: Response) => { }; export const deleteWebhook = async (req: Request, res: Response) => { - const { webhookId } = req.params; + const { + params: { webhookId } + } = await validateRequest(reqValidator.DeleteWebhookV1, req); let webhook = await Webhook.findById(webhookId); if (!webhook) { throw ResourceNotFoundError({ message: "Webhook not found!!" }); } - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: webhook.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); - + const { permission } = await getUserProjectPermissions( + req.user._id, + webhook.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Webhooks + ); + webhook = await Webhook.findByIdAndDelete(webhookId); if (!webhook) { @@ -139,17 +165,23 @@ export const deleteWebhook = async (req: Request, res: Response) => { }; export const testWebhook = async (req: Request, res: Response) => { - const { webhookId } = req.params; + const { + params: { webhookId } + } = await validateRequest(reqValidator.TestWebhookV1, req); + const webhook = await Webhook.findById(webhookId); if (!webhook) { throw BadRequestError({ message: "Webhook not found!!" }); } - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: webhook.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); + const { permission } = await getUserProjectPermissions( + req.user._id, + webhook.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Webhooks + ); try { await triggerWebhookRequest( @@ -182,7 +214,15 @@ export const testWebhook = async (req: Request, res: Response) => { }; export const listWebhooks = async (req: Request, res: Response) => { - const { environment, workspaceId, secretPath } = req.query; + const { + query: { environment, workspaceId, secretPath } + } = await validateRequest(reqValidator.ListWebhooksV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Webhooks + ); const optionalFilters: Record = {}; if (environment) optionalFilters.environment = environment as string; diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index a655e9690..29ed27297 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -20,6 +20,13 @@ import { getUserOrgPermissions } from "../../services/RoleService"; import { ForbiddenError } from "@casl/ability"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; /** * Return public keys of members of workspace with id [workspaceId] @@ -28,7 +35,15 @@ import { ForbiddenError } from "@casl/ability"; * @returns */ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Member + ); const publicKeys = ( await Membership.find({ @@ -53,7 +68,15 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceMemberships = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Member + ); const users = await Membership.find({ workspace: workspaceId @@ -89,7 +112,9 @@ export const getWorkspaces = async (req: Request, res: Response) => { * @returns */ export const getWorkspace = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceV1, req); const workspace = await Workspace.findOne({ _id: workspaceId @@ -108,7 +133,9 @@ export const getWorkspace = async (req: Request, res: Response) => { * @returns */ export const createWorkspace = async (req: Request, res: Response) => { - const { workspaceName, organizationId } = req.body; + const { + body: { organizationId, workspaceName } + } = await validateRequest(reqValidator.CreateWorkspaceV1, req); const organization = await Organization.findById(organizationId); if (!organization) { @@ -183,8 +210,16 @@ export const deleteWorkspace = async (req: Request, res: Response) => { * @returns */ export const changeWorkspaceName = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { name } = req.body; + const { + params: { workspaceId }, + body: { name } + } = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Workspace + ); const workspace = await Workspace.findOneAndUpdate( { @@ -229,7 +264,15 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); const authorizations = await IntegrationAuth.find({ workspace: workspaceId @@ -247,7 +290,16 @@ export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: R * @returns */ export const getWorkspaceServiceTokens = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.ServiceTokens + ); + // ?? FIX. const serviceTokens = await ServiceToken.find({ user: req.user._id, diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 8f2f4cea0..d75206c64 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -10,16 +10,11 @@ import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELogService } from "../../ee/services"; import { BadRequestError, InternalServerError } from "../../utils/errors"; -import { - ACTION_LOGIN, - TOKEN_EMAIL_MFA, -} from "../../variables"; +import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; import { getUserAgentType } from "../../utils/posthog"; // TODO: move this -import { - getHttpsEnabled, - getJwtMfaLifetime, - getJwtMfaSecret, -} from "../../config"; +import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -34,13 +29,10 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - clientPublicKey, - }: { email: string; clientPublicKey: string } = req.body; + const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier"); if (!user) throw new Error("Failed to find user"); @@ -49,25 +41,28 @@ export const login1 = async (req: Request, res: Response) => { server.init( { salt: user.salt, - verifier: user.verifier, + verifier: user.verifier }, async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }); + await LoginSRPDetail.findOneAndReplace( + { email: email }, + { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); return res.status(200).send({ serverPublicKey, - salt: user.salt, + salt: user.salt }); } ); - }; /** @@ -78,19 +73,22 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" }); + if (!req.headers["user-agent"]) + throw InternalServerError({ message: "User-Agent header is required" }); const { email, clientProof } = req.body; const user = await User.findOne({ - email, - }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); + email + }).select( + "+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices" + ); if (!user) throw new Error("Failed to find user"); - const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!loginSRPDetail) { - return BadRequestError(Error("Failed to find login details for SRP")) + return BadRequestError(Error("Failed to find login details for SRP")); } const server = new jsrp.server(); @@ -98,7 +96,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetail.serverBInt, + b: loginSRPDetail.serverBInt }, async () => { server.setClientPublicKey(loginSRPDetail.clientPublicKey); @@ -111,15 +109,15 @@ export const login2 = async (req: Request, res: Response) => { // generate temporary MFA token const token = createToken({ payload: { - userId: user._id.toString(), + userId: user._id.toString() }, expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret(), + secret: await getJwtMfaSecret() }); const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email, + email }); // send MFA code [code] to [email] @@ -128,27 +126,27 @@ export const login2 = async (req: Request, res: Response) => { subjectLine: "Infisical MFA code", recipients: [email], substitutions: { - code, - }, + code + } }); return res.status(200).send({ mfaEnabled: true, - token, + token }); } await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // issue tokens - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -156,7 +154,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); // case: user does not have MFA enabled @@ -182,36 +180,33 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag, - } + tag: user.tag + }; - if ( - user?.protectedKey && - user?.protectedKeyIV && - user?.protectedKeyTag - ) { + if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { response.protectedKey = user.protectedKey; - response.protectedKeyIV = user.protectedKeyIV + response.protectedKeyIV = user.protectedKeyIV; response.protectedKeyTag = user.protectedKeyTag; } const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.ip, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.ip + })); return res.status(200).send(response); } return res.status(400).send({ - message: "Failed to authenticate. Try again?", + message: "Failed to authenticate. Try again?" }); } ); @@ -219,15 +214,17 @@ export const login2 = async (req: Request, res: Response) => { /** * Send MFA token to email [email] - * @param req - * @param res + * @param req + * @param res */ export const sendMfaToken = async (req: Request, res: Response) => { - const { email } = req.body; + const { + body: { email } + } = await validateRequest(reqValidator.SendMfaTokenV2, req); const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email, + email }); // send MFA code [code] to [email] @@ -236,49 +233,53 @@ export const sendMfaToken = async (req: Request, res: Response) => { subjectLine: "Infisical MFA code", recipients: [email], substitutions: { - code, - }, + code + } }); return res.status(200).send({ - message: "Successfully sent new MFA code", + message: "Successfully sent new MFA code" }); -} +}; /** * Verify MFA token [mfaToken] and issue JWT and refresh tokens if the * MFA token [mfaToken] is valid - * @param req - * @param res + * @param req + * @param res */ export const verifyMfaToken = async (req: Request, res: Response) => { - const { email, mfaToken } = req.body; + const { + body: { email, mfaToken } + } = await validateRequest(reqValidator.VerifyMfaTokenV2, req); await TokenService.validateToken({ type: TOKEN_EMAIL_MFA, email, - token: mfaToken, + token: mfaToken }); const user = await User.findOne({ - email, - }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); + email + }).select( + "+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices" + ); if (!user) throw new Error("Failed to find user"); - await LoginSRPDetail.deleteOne({ userId: user.id }) + await LoginSRPDetail.deleteOne({ userId: user.id }); await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // issue tokens - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -286,7 +287,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); interface VerifyMfaTokenRes { @@ -319,8 +320,8 @@ export const verifyMfaToken = async (req: Request, res: Response) => { publicKey: user.publicKey as string, encryptedPrivateKey: user.encryptedPrivateKey as string, iv: user.iv as string, - tag: user.tag as string, - } + tag: user.tag as string + }; if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { resObj.protectedKey = user.protectedKey; @@ -330,15 +331,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => { const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); return res.status(200).send(resObj); -} +}; diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 4e7d39fab..8137029dc 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -6,13 +6,21 @@ import { Secret, ServiceToken, ServiceTokenData, - Workspace, + Workspace } from "../../models"; import { EventType, SecretVersion } from "../../ee/models"; import { EEAuditLogService, EELicenseService } from "../../ee/services"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import _ from "lodash"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/environments"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Create new workspace environment named [environmentName] under workspace with id @@ -20,13 +28,18 @@ import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variabl * @param res * @returns */ -export const createWorkspaceEnvironment = async ( - req: Request, - res: Response -) => { +export const createWorkspaceEnvironment = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + body: { environmentName, environmentSlug } + } = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Environments + ); - const { workspaceId } = req.params; - const { environmentName, environmentSlug } = req.body; const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) throw WorkspaceNotFoundError(); @@ -39,7 +52,8 @@ export const createWorkspaceEnvironment = async ( // case: number of environments used exceeds the number of environments allowed return res.status(400).send({ - message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.", + message: + "Failed to create environment due to environment limit reached. Upgrade plan to create more environments." }); } } @@ -55,7 +69,7 @@ export const createWorkspaceEnvironment = async ( workspace?.environments.push({ name: environmentName, - slug: environmentSlug.toLowerCase(), + slug: environmentSlug.toLowerCase() }); await workspace.save(); @@ -80,8 +94,8 @@ export const createWorkspaceEnvironment = async ( workspace: workspaceId, environment: { name: environmentName, - slug: environmentSlug, - }, + slug: environmentSlug + } }); }; @@ -129,12 +143,18 @@ export const reorderWorkspaceEnvironments = async ( * @param res * @returns */ -export const renameWorkspaceEnvironment = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; +export const renameWorkspaceEnvironment = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + body: { environmentName, environmentSlug, oldEnvironmentSlug } + } = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Environments + ); + // user should pass both new slug and env name if (!environmentSlug || !environmentName) { throw new Error("Invalid environment given."); @@ -148,16 +168,13 @@ export const renameWorkspaceEnvironment = async ( const isEnvExist = workspace.environments.some( ({ name, slug }) => - slug !== oldEnvironmentSlug && - (name === environmentName || slug === environmentSlug) + slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug) ); if (isEnvExist) { throw new Error("Invalid environment given"); } - const envIndex = workspace?.environments.findIndex( - ({ slug }) => slug === oldEnvironmentSlug - ); + const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug); if (envIndex === -1) { throw new Error("Invalid environment given"); } @@ -191,7 +208,7 @@ export const renameWorkspaceEnvironment = async ( await Membership.updateMany( { workspace: workspaceId, - "deniedPermissions.environmentSlug": oldEnvironmentSlug, + "deniedPermissions.environmentSlug": oldEnvironmentSlug }, { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } @@ -218,8 +235,8 @@ export const renameWorkspaceEnvironment = async ( workspace: workspaceId, environment: { name: environmentName, - slug: environmentSlug, - }, + slug: environmentSlug + } }); }; @@ -229,21 +246,25 @@ export const renameWorkspaceEnvironment = async ( * @param res * @returns */ -export const deleteWorkspaceEnvironment = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environmentSlug } = req.body; +export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + body: { environmentSlug } + } = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Environments + ); + // atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) { throw new Error("Failed to create workspace environment"); } - const envIndex = workspace?.environments.findIndex( - ({ slug }) => slug === environmentSlug - ); + const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug); if (envIndex === -1) { throw new Error("Invalid environment given"); } @@ -256,11 +277,11 @@ export const deleteWorkspaceEnvironment = async ( // clean up await Secret.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); await SecretVersion.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); // await ServiceToken.deleteMany({ @@ -279,7 +300,7 @@ export const deleteWorkspaceEnvironment = async ( await Integration.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); await Membership.updateMany( { workspace: workspaceId }, @@ -305,46 +326,48 @@ export const deleteWorkspaceEnvironment = async ( return res.status(200).send({ message: "Successfully deleted environment", workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); }; +// TODO(akhilmhdh) after rbac this can be completely removed +export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetAllAccessibileEnvironmentsOfWorkspaceV2, req); -export const getAllAccessibleEnvironmentsOfWorkspace = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const workspacesUserIsMemberOf = await Membership.findOne({ - workspace: workspaceId, - user: req.user, - }) + const { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions( + req.user._id, + workspaceId + ); - if (!workspacesUserIsMemberOf) { - throw BadRequestError() - } + const accessibleEnvironments: any = []; + const deniedPermission = workspacesUserIsMemberOf.deniedPermissions; - const accessibleEnvironments: any = [] - const deniedPermission = workspacesUserIsMemberOf.deniedPermissions - - const relatedWorkspace = await Workspace.findById(workspaceId) + const relatedWorkspace = await Workspace.findById(workspaceId); if (!relatedWorkspace) { - throw BadRequestError() + throw BadRequestError(); } - relatedWorkspace.environments.forEach(environment => { - const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS }) - const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS }) + relatedWorkspace.environments.forEach((environment) => { + const isReadBlocked = _.some(deniedPermission, { + environmentSlug: environment.slug, + ability: PERMISSION_READ_SECRETS + }); + const isWriteBlocked = _.some(deniedPermission, { + environmentSlug: environment.slug, + ability: PERMISSION_WRITE_SECRETS + }); if (isReadBlocked && isWriteBlocked) { - return + return; } else { accessibleEnvironments.push({ name: environment.name, slug: environment.slug, isWriteDenied: isWriteBlocked, - isReadDenied: isReadBlocked, - }) + isReadDenied: isReadBlocked + }); } - }) + }); - res.json({ accessibleEnvironments }) + res.json({ accessibleEnvironments }); }; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 6c9b29203..60fd983e4 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -25,7 +25,7 @@ import { userHasWriteOnlyAbility } from "../../ee/helpers/checkMembershipPermissions"; import _ from "lodash"; -import { BatchSecret, BatchSecretRequest } from "../../types/secret"; +import { BatchSecret } from "../../types/secret"; import { getFolderByPath, getFolderIdFromServiceToken, @@ -35,6 +35,8 @@ import { import { isValidScope } from "../../helpers/secrets"; import path from "path"; import { getAllImportedSecrets } from "../../services/SecretImportService"; +import { validateRequest } from "../../helpers/validation"; +import { BatchSecretsV2, GetSecretsV2 } from "../../validation"; /** * Peform a batch of any specified CUD secret operations @@ -46,22 +48,17 @@ export const batchSecrets = async (req: Request, res: Response) => { const channel = getUserAgentType(req.headers["user-agent"]); const postHogClient = await TelemetryService.getPostHogClient(); + const validatedData = await validateRequest(BatchSecretsV2, req); const { - workspaceId, - environment, - requests - }: { - workspaceId: string; - environment: string; - requests: BatchSecretRequest[]; - } = req.body; - - let secretPath = req.body.secretPath as string; - let folderId = req.body.folderId as string; + body: { workspaceId, environment, requests } + } = validatedData; + let { + body: { secretPath, folderId } + } = validatedData; const createSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = []; - const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = []; + const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = []; const actions: IAction[] = []; // get secret blind index salt @@ -72,7 +69,11 @@ export const batchSecrets = async (req: Request, res: Response) => { const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (req.authData.authPayload instanceof ServiceTokenData) { - const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + environment, + secretPath || "/" + ); // in service token when not giving secretpath folderid must be root // this is to avoid giving folderid when service tokens are used @@ -133,7 +134,10 @@ export const batchSecrets = async (req: Request, res: Response) => { }); break; case "DELETE": - deleteSecrets.push({ _id: new Types.ObjectId(request.secret._id), secretName: request.secret.secretName }); + deleteSecrets.push({ + _id: new Types.ObjectId(request.secret._id), + secretName: request.secret.secretName + }); break; } } @@ -332,18 +336,19 @@ export const batchSecrets = async (req: Request, res: Response) => { if (deleteSecrets.length > 0) { const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id); - const deletedSecretsObj = (await Secret.find({ - _id: { - $in: deleteSecretIds - } - })) - .reduce( - (obj: any, secret: ISecret) => ({ - ...obj, - [secret._id.toString()]: secret - }), - {} - ); + const deletedSecretsObj = ( + await Secret.find({ + _id: { + $in: deleteSecretIds + } + }) + ).reduce( + (obj: any, secret: ISecret) => ({ + ...obj, + [secret._id.toString()]: secret + }), + {} + ); await Secret.deleteMany({ _id: { @@ -781,10 +786,13 @@ export const getSecrets = async (req: Request, res: Response) => { } */ - const { tagSlugs, secretPath, include_imports } = req.query; - let { folderId } = req.query; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; + const validatedData = await validateRequest(GetSecretsV2, req); + const { + query: { tagSlugs, secretPath, include_imports, workspaceId, environment } + } = validatedData; + let { + query: { folderId } + } = validatedData; const folders = await Folder.findOne({ workspace: workspaceId, environment }); @@ -926,7 +934,7 @@ export const getSecrets = async (req: Request, res: Response) => { // TODO(akhilmhdh) - secret-imp change this to org type let importedSecrets: any[] = []; - if (include_imports === "true") { + if (include_imports) { importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string); } diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index c918e6953..28168675d 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -6,6 +6,15 @@ import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; import { ActorType, EventType } from "../../ee/models"; import { EEAuditLogService } from "../../ee/services"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/serviceTokenData"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { Types } from "mongoose"; /** * Return service token data associated with service token on request @@ -63,7 +72,14 @@ export const getServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => { let serviceTokenData; - const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body; + const { + body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv } + } = await validateRequest(reqValidator.CreateServiceTokenV2, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.ServiceTokens + ); const secret = crypto.randomBytes(16).toString("hex"); const secretHash = await bcrypt.hash(secret, await getSaltRounds()); @@ -75,7 +91,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { } let user; - + if (req.authData.actor.type === ActorType.USER) { user = req.authData.authPayload._id; } @@ -100,7 +116,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { if (!serviceTokenData) throw new Error("Failed to find service token data"); const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; - + await EEAuditLogService.createAuditLog( req.authData, { @@ -111,7 +127,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { } }, { - workspaceId + workspaceId: new Types.ObjectId(workspaceId) } ); @@ -128,14 +144,29 @@ export const createServiceTokenData = async (req: Request, res: Response) => { * @returns */ export const deleteServiceTokenData = async (req: Request, res: Response) => { - const { serviceTokenDataId } = req.params; + const { + params: { serviceTokenDataId } + } = await validateRequest(reqValidator.DeleteServiceTokenV2, req); + + let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId); + if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + serviceTokenData.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.ServiceTokens + ); + + serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + if (!serviceTokenData) + return res.status(200).send({ + message: "Failed to delete service token" + }); - const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - - if (!serviceTokenData) return res.status(200).send({ - message: "Failed to delete service token" - }); - await EEAuditLogService.createAuditLog( req.authData, { diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 4a7e68bb7..898d89045 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -1,42 +1,58 @@ +import { ForbiddenError } from "@casl/ability"; import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Membership, Secret, Tag } from "../../models"; -import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { Secret, Tag } from "../../models"; +import { BadRequestError } from "../../utils/errors"; +import { validateRequest } from "../../helpers/validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import * as reqValidator from "../../validation/tags"; export const createWorkspaceTag = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { name, slug, tagColor } = req.body; - + const { + body: { name, slug }, + params: { workspaceId } + } = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Tags + ); + const tagToCreate = { - name, - tagColor, - workspace: new Types.ObjectId(workspaceId), - slug, - user: new Types.ObjectId(req.user._id), - }; - + name, + workspace: new Types.ObjectId(workspaceId), + slug, + user: new Types.ObjectId(req.user._id) + }; + const createdTag = await new Tag(tagToCreate).save(); - + res.json(createdTag); }; export const deleteWorkspaceTag = async (req: Request, res: Response) => { - const { tagId } = req.params; + const { + params: { tagId } + } = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req); const tagFromDB = await Tag.findById(tagId); if (!tagFromDB) { throw BadRequestError(); } - // can only delete if the request user is one that belongs to the same workspace as the tag - const membership = await Membership.findOne({ - user: req.user, - workspace: tagFromDB.workspace - }); - - if (!membership) { - UnauthorizedRequestError({ message: "Failed to validate membership" }); - } + const { permission } = await getUserProjectPermissions( + req.user._id, + tagFromDB.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Tags + ); const result = await Tag.findByIdAndDelete(tagId); @@ -47,12 +63,19 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => { }; export const getWorkspaceTags = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - - const workspaceTags = await Tag.find({ - workspace: new Types.ObjectId(workspaceId) + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceTagsV2, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Tags + ); + + const workspaceTags = await Tag.find({ + workspace: new Types.ObjectId(workspaceId) }); - + return res.json({ workspaceTags }); diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 9e23ab8ac..0b859f749 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -2,23 +2,19 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import crypto from "crypto"; import bcrypt from "bcrypt"; -import { - APIKeyData, - AuthMethod, - MembershipOrg, - TokenVersion, - User -} from "../../models"; +import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models"; import { getSaltRounds } from "../../config"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation"; /** * Return the current user. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getMe = async (req: Request, res: Response) => { - /* + /* #swagger.summary = "Retrieve the current user on the request" #swagger.description = "Retrieve the current user on the request" @@ -43,124 +39,117 @@ export const getMe = async (req: Request, res: Response) => { } } */ - const user = await User - .findById(req.user._id) - .select("+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"); - - return res.status(200).send({ - user, - }); -} + const user = await User.findById(req.user._id).select( + "+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag" + ); + + return res.status(200).send({ + user + }); +}; /** * Update the current user's MFA-enabled status [isMfaEnabled]. * Note: Infisical currently only supports email-based 2FA only; this will expand to * include SMS and authenticator app modes of authentication in the future. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateMyMfaEnabled = async (req: Request, res: Response) => { - const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body; - req.user.isMfaEnabled = isMfaEnabled; - - if (isMfaEnabled) { - // TODO: adapt this route/controller - // to work for different forms of MFA - req.user.mfaMethods = ["email"]; - } else { - req.user.mfaMethods = []; - } + const { + body: { isMfaEnabled } + } = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req); - await req.user.save(); - - const user = req.user; - - return res.status(200).send({ - user, - }); -} + req.user.isMfaEnabled = isMfaEnabled; + + if (isMfaEnabled) { + // TODO: adapt this route/controller + // to work for different forms of MFA + req.user.mfaMethods = ["email"]; + } else { + req.user.mfaMethods = []; + } + + await req.user.save(); + + const user = req.user; + + return res.status(200).send({ + user + }); +}; /** * Update name of the current user to [firstName, lastName]. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateName = async (req: Request, res: Response) => { - const { - firstName, - lastName - }: { - firstName: string; - lastName: string; - } = req.body; - - const user = await User.findByIdAndUpdate( - req.user._id.toString(), - { - firstName, - lastName: lastName ?? "" - }, - { - new: true - } - ); - - return res.status(200).send({ - user, - }); -} + const { + body: { lastName, firstName } + } = await validateRequest(reqValidator.UpdateNameV2, req); + + const user = await User.findByIdAndUpdate( + req.user._id.toString(), + { + firstName, + lastName: lastName ?? "" + }, + { + new: true + } + ); + + return res.status(200).send({ + user + }); +}; /** * Update auth method of the current user to [authMethods] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ - export const updateAuthMethods = async (req: Request, res: Response) => { - const { - authMethods - } = req.body; - - const hasSamlEnabled = req.user.authMethods - .some( - (authMethod: AuthMethod) => [ - AuthMethod.OKTA_SAML, - AuthMethod.AZURE_SAML, - AuthMethod.JUMPCLOUD_SAML - ].includes(authMethod) - ); +export const updateAuthMethods = async (req: Request, res: Response) => { + const { + body: { authMethods } + } = await validateRequest(reqValidator.UpdateAuthMethodsV2, req); - if (hasSamlEnabled) { - return res.status(400).send({ - message: "Failed to update user authentication method because SAML SSO is enforced" - }); - } + const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) => + [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod) + ); - const user = await User.findByIdAndUpdate( - req.user._id.toString(), - { - authMethods - }, - { - new: true - } - ); - - return res.status(200).send({ - user + if (hasSamlEnabled) { + return res.status(400).send({ + message: "Failed to update user authentication method because SAML SSO is enforced" }); -} + } + const user = await User.findByIdAndUpdate( + req.user._id.toString(), + { + authMethods + }, + { + new: true + } + ); + + return res.status(200).send({ + user + }); +}; /** * Return organizations that the current user is part of. - * @param req - * @param res + * @param req + * @param res */ export const getMyOrganizations = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return organizations that current user is part of' #swagger.description = 'Return organizations that current user is part of' @@ -189,114 +178,121 @@ export const getMyOrganizations = async (req: Request, res: Response) => { */ const organizations = ( await MembershipOrg.find({ - user: req.user._id, + user: req.user._id }).populate("organization") ).map((m) => m.organization); - return res.status(200).send({ - organizations, - }); -} + return res.status(200).send({ + organizations + }); +}; /** * Return API keys belonging to current user. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getMyAPIKeys = async (req: Request, res: Response) => { - const apiKeyData = await APIKeyData.find({ - user: req.user._id, - }); + const apiKeyData = await APIKeyData.find({ + user: req.user._id + }); - return res.status(200).send(apiKeyData); -} + return res.status(200).send(apiKeyData); +}; /** * Create new API key for current user. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createAPIKey = async (req: Request, res: Response) => { - const { name, expiresIn } = req.body; + const { + body: { name, expiresIn } + } = await validateRequest(reqValidator.CreateApiKeyV2, req); - const secret = crypto.randomBytes(16).toString("hex"); - const secretHash = await bcrypt.hash(secret, await getSaltRounds()); + const secret = crypto.randomBytes(16).toString("hex"); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - let apiKeyData = await new APIKeyData({ - name, - lastUsed: new Date(), - expiresAt, - user: req.user._id, - secretHash, - }).save(); + let apiKeyData = await new APIKeyData({ + name, + lastUsed: new Date(), + expiresAt, + user: req.user._id, + secretHash + }).save(); - // return api key data without sensitive data - apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any; + // return api key data without sensitive data + apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any; - if (!apiKeyData) throw new Error("Failed to find API key data"); + if (!apiKeyData) throw new Error("Failed to find API key data"); - const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; + const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; - return res.status(200).send({ - apiKey, - apiKeyData, - }); -} + return res.status(200).send({ + apiKey, + apiKeyData + }); +}; /** * Delete API key with id [apiKeyDataId] belonging to current user - * @param req - * @param res + * @param req + * @param res */ export const deleteAPIKey = async (req: Request, res: Response) => { - const { apiKeyDataId } = req.params; + const { + params: { apiKeyDataId } + } = await validateRequest(reqValidator.DeleteApiKeyV2, req); - const apiKeyData = await APIKeyData.findOneAndDelete({ - _id: new Types.ObjectId(apiKeyDataId), - user: req.user._id - }); + const apiKeyData = await APIKeyData.findOneAndDelete({ + _id: new Types.ObjectId(apiKeyDataId), + user: req.user._id + }); - return res.status(200).send({ - apiKeyData - }); -} + return res.status(200).send({ + apiKeyData + }); +}; /** * Return active sessions (TokenVersion) belonging to user - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getMySessions = async (req: Request, res: Response) => { - const tokenVersions = await TokenVersion.find({ - user: req.user._id - }); - - return res.status(200).send(tokenVersions); -} + const tokenVersions = await TokenVersion.find({ + user: req.user._id + }); + + return res.status(200).send(tokenVersions); +}; /** * Revoke all active sessions belong to user - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteMySessions = async (req: Request, res: Response) => { - await TokenVersion.updateMany({ - user: req.user._id, - }, { - $inc: { - refreshVersion: 1, - accessVersion: 1, - }, - }); + await TokenVersion.updateMany( + { + user: req.user._id + }, + { + $inc: { + refreshVersion: 1, + accessVersion: 1 + } + } + ); - return res.status(200).send({ - message: "Successfully revoked all sessions" - }); -} \ No newline at end of file + return res.status(200).send({ + message: "Successfully revoked all sessions" + }); +}; diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index f057298be..1905aa292 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -11,6 +11,14 @@ import { EventService, TelemetryService } from "../../services"; import { eventPushSecrets } from "../../events"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; interface V2PushSecret { type: string; // personal or shared @@ -181,15 +189,17 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { } } */ - const { workspaceId } = req.params; - + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceKeyV2, req); + const key = await Key.findOne({ workspace: workspaceId, receiver: req.user._id }).populate("sender", "+publicKey"); if (!key) throw new Error("Failed to find workspace key"); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -258,7 +268,15 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { } } */ - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Member + ); const memberships = await Membership.find({ workspace: workspaceId @@ -329,8 +347,16 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) => } } */ - const { membershipId } = req.params; - const { role } = req.body; + const { + params: { workspaceId, membershipId }, + body: { role } + } = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); const membership = await Membership.findByIdAndUpdate( membershipId, @@ -390,7 +416,15 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => } } */ - const { membershipId } = req.params; + const { + params: { workspaceId, membershipId } + } = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Member + ); const membership = await Membership.findByIdAndDelete(membershipId); @@ -413,8 +447,16 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => * @returns */ export const toggleAutoCapitalization = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { autoCapitalization } = req.body; + const { + params: { workspaceId }, + body: { autoCapitalization } + } = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Settings + ); const workspace = await Workspace.findOneAndUpdate( { diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 413bcefa0..1b228a39b 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -10,25 +10,20 @@ import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELogService } from "../../ee/services"; import { BadRequestError, InternalServerError } from "../../utils/errors"; -import { - ACTION_LOGIN, - TOKEN_EMAIL_MFA, -} from "../../variables"; +import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; import { getUserAgentType } from "../../utils/posthog"; // TODO: move this -import { - getHttpsEnabled, - getJwtMfaLifetime, - getJwtMfaSecret, -} from "../../config"; +import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; import { AuthMethod } from "../../models/user"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { - export interface ProviderAuthJwtPayload extends jwt.JwtPayload { - userId: string; - email: string; - authProvider: AuthMethod; - isUserCompleted: boolean, - } + export interface ProviderAuthJwtPayload extends jwt.JwtPayload { + userId: string; + email: string; + authProvider: AuthMethod; + isUserCompleted: boolean; + } } /** @@ -38,53 +33,51 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - providerAuthToken, - clientPublicKey, - }: { - email: string; - clientPublicKey: string, - providerAuthToken?: string; - } = req.body; - - const user = await User.findOne({ - email, - }).select("+salt +verifier"); + const { + body: { email, clientPublicKey, providerAuthToken } + } = await validateRequest(reqValidator.Login1V3, req); - if (!user) throw new Error("Failed to find user"); - - if (!user.authMethods.includes(AuthMethod.EMAIL)) { - await validateProviderAuthToken({ - email, - providerAuthToken, - }); - } + const user = await User.findOne({ + email + }).select("+salt +verifier"); - const server = new jsrp.server(); - server.init( + if (!user) throw new Error("Failed to find user"); + + if (!user.authMethods.includes(AuthMethod.EMAIL)) { + await validateProviderAuthToken({ + email, + providerAuthToken + }); + } + + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); + await LoginSRPDetail.findOneAndReplace( { - salt: user.salt, - verifier: user.verifier, + email: email }, - async () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ - email: email, - }, { - email, - userId: user.id, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }); + { + email, + userId: user.id, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); - return res.status(200).send({ - serverPublicKey, - salt: user.salt, - }); - } - ); + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); }; /** @@ -95,150 +88,151 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" }); + if (!req.headers["user-agent"]) + throw InternalServerError({ message: "User-Agent header is required" }); - const { email, clientProof, providerAuthToken } = req.body; + const { + body: { email, providerAuthToken, clientProof } + } = await validateRequest(reqValidator.Login2V3, req); - const user = await User.findOne({ - email, - }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); + const user = await User.findOne({ + email + }).select( + "+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices" + ); - if (!user) throw new Error("Failed to find user"); - - if (!user.authMethods.includes(AuthMethod.EMAIL)) { - await validateProviderAuthToken({ - email, - providerAuthToken, - }) - } + if (!user) throw new Error("Failed to find user"); - const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) + if (!user.authMethods.includes(AuthMethod.EMAIL)) { + await validateProviderAuthToken({ + email, + providerAuthToken + }); + } - if (!loginSRPDetail) { - return BadRequestError(Error("Failed to find login details for SRP")) - } + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: loginSRPDetail.serverBInt, - }, - async () => { - server.setClientPublicKey(loginSRPDetail.clientPublicKey); + if (!loginSRPDetail) { + return BadRequestError(Error("Failed to find login details for SRP")); + } - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetail.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetail.clientPublicKey); - if (user.isMfaEnabled) { - // case: user has MFA enabled + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + if (user.isMfaEnabled) { + // case: user has MFA enabled - // generate temporary MFA token - const token = createToken({ - payload: { - userId: user._id.toString(), - }, - expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret(), - }); + // generate temporary MFA token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: await getJwtMfaLifetime(), + secret: await getJwtMfaSecret() + }); - const code = await TokenService.createToken({ - type: TOKEN_EMAIL_MFA, - email, - }); + const code = await TokenService.createToken({ + type: TOKEN_EMAIL_MFA, + email + }); - // send MFA code [code] to [email] - await sendMail({ - template: "emailMfa.handlebars", - subjectLine: "Infisical MFA code", - recipients: [user.email], - substitutions: { - code, - }, - }); - - return res.status(200).send({ - mfaEnabled: true, - token, - }); - } - - await checkUserDevice({ - user, - ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - }); - - // issue tokens - const tokens = await issueAuthTokens({ - userId: user._id, - ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - }); - - // store (refresh) token in httpOnly cookie - res.cookie("jid", tokens.refreshToken, { - httpOnly: true, - path: "/", - sameSite: "strict", - secure: await getHttpsEnabled(), - }); - - // case: user does not have MFA enablgged - // return (access) token in response - - interface ResponseData { - mfaEnabled: boolean; - encryptionVersion: any; - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - token: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; - } - - const response: ResponseData = { - mfaEnabled: false, - encryptionVersion: user.encryptionVersion, - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag, - } - - if ( - user?.protectedKey && - user?.protectedKeyIV && - user?.protectedKeyTag - ) { - response.protectedKey = user.protectedKey; - response.protectedKeyIV = user.protectedKeyIV - response.protectedKeyTag = user.protectedKeyTag; - } - - const loginAction = await EELogService.createAction({ - name: ACTION_LOGIN, - userId: user._id, - }); - - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); - - return res.status(200).send(response); + // send MFA code [code] to [email] + await sendMail({ + template: "emailMfa.handlebars", + subjectLine: "Infisical MFA code", + recipients: [user.email], + substitutions: { + code } + }); - return res.status(400).send({ - message: "Failed to authenticate. Try again?", - }); + return res.status(200).send({ + mfaEnabled: true, + token + }); } - ); + + await checkUserDevice({ + user, + ip: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); + + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); + + // store (refresh) token in httpOnly cookie + res.cookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled() + }); + + // case: user does not have MFA enablgged + // return (access) token in response + + interface ResponseData { + mfaEnabled: boolean; + encryptionVersion: any; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; + } + + const response: ResponseData = { + mfaEnabled: false, + encryptionVersion: user.encryptionVersion, + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + }; + + if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { + response.protectedKey = user.protectedKey; + response.protectedKeyIV = user.protectedKeyIV; + response.protectedKeyTag = user.protectedKeyTag; + } + + const loginAction = await EELogService.createAction({ + name: ACTION_LOGIN, + userId: user._id + }); + + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); + + return res.status(200).send(response); + } + + return res.status(400).send({ + message: "Failed to authenticate. Try again?" + }); + } + ); }; diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 4a57beba3..93e3da885 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -9,8 +9,14 @@ import { getAllImportedSecrets } from "../../services/SecretImportService"; import { Folder, IServiceTokenData } from "../../models"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; -import { requireWorkspaceAuth } from "../../middleware"; -import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/secrets"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError, subject } from "@casl/ability"; /** * Return secrets for workspace with id [workspaceId] and environment @@ -19,31 +25,29 @@ import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; * @param res */ export const getSecretsRaw = async (req: Request, res: Response) => { - let workspaceId = req.query.workspaceId as string; - let environment = req.query.environment as string; - let secretPath = req.query.secretPath as string; - const folderId = req.query.folderId as string | undefined; - const includeImports = req.query.include_imports as string; + let { + query: { secretPath, environment, workspaceId, include_imports: includeImports, folderId } + } = await validateRequest(reqValidator.GetSecretsRawV3, req); // if the service token has single scope, it will get all secrets for that scope by default const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData; - if (serviceTokenDetails && serviceTokenDetails.scopes.length == 1 && !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)) { + if ( + serviceTokenDetails && + serviceTokenDetails.scopes.length == 1 && + !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath) + ) { const scope = serviceTokenDetails.scopes[0]; secretPath = scope.secretPath; environment = scope.environment; workspaceId = serviceTokenDetails.workspace.toString(); } else { - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true - }); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); } - const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, @@ -56,7 +60,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId) }); - if (includeImports === "true") { + if (includeImports) { const folders = await Folder.findOne({ workspace: workspaceId, environment }); let folderId = "root"; // if folder exist get it and replace folderid with new one @@ -99,11 +103,18 @@ export const getSecretsRaw = async (req: Request, res: Response) => { * @param res */ export const getSecretByNameRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; - const type = req.query.type as "shared" | "personal" | undefined; + const { + query: { secretPath, environment, workspaceId, type }, + params: { secretName } + } = await validateRequest(reqValidator.GetSecretByNameRawV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const secret = await SecretService.getSecret({ secretName, @@ -132,8 +143,18 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { * @param res */ export const createSecretRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body; + const { + params: { secretName }, + body: { secretPath, environment, workspaceId, type, secretValue, secretComment } + } = await validateRequest(reqValidator.CreateSecretRawV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -197,8 +218,18 @@ export const createSecretRaw = async (req: Request, res: Response) => { * @param res */ export const updateSecretByNameRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body; + const { + params: { secretName }, + body: { secretValue, environment, secretPath, type, workspaceId } + } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -211,7 +242,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { const secret = await SecretService.updateSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -243,12 +274,22 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { * @param res */ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretPath = "/" } = req.body; + const { + params: { secretName }, + body: { environment, secretPath, type, workspaceId } + } = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const { secret } = await SecretService.deleteSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -282,11 +323,17 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { * @param res */ export const getSecrets = async (req: Request, res: Response) => { - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; - const folderId = req.query.folderId as string | undefined; - const includeImports = req.query.include_imports as string; + const { + query: { secretPath, environment, workspaceId, include_imports: includeImports,folderId } + } = await validateRequest(reqValidator.GetSecretsV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -296,7 +343,7 @@ export const getSecrets = async (req: Request, res: Response) => { authData: req.authData }); - if (includeImports === "true") { + if (includeImports) { const folders = await Folder.findOne({ workspace: workspaceId, environment }); let folderId = "root"; // if folder exist get it and replace folderid with new one @@ -325,11 +372,18 @@ export const getSecrets = async (req: Request, res: Response) => { * @param res */ export const getSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; - const type = req.query.type as "shared" | "personal" | undefined; + const { + query: { secretPath, environment, workspaceId, type }, + params: { secretName } + } = await validateRequest(reqValidator.GetSecretByNameV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const secret = await SecretService.getSecret({ secretName, @@ -351,23 +405,33 @@ export const getSecretByName = async (req: Request, res: Response) => { * @param res */ export const createSecret = async (req: Request, res: Response) => { - const { secretName } = req.params; const { - workspaceId, - environment, - type, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretPath = "/", - metadata - } = req.body; + body: { + workspaceId, + secretName, + secretPath, + environment, + metadata, + type, + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext + } + } = await validateRequest(reqValidator.CreateSecretV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const secret = await SecretService.createSecret({ secretName, @@ -410,20 +474,30 @@ export const createSecret = async (req: Request, res: Response) => { * @param res */ export const updateSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; const { - workspaceId, - environment, - type, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretPath = "/" - } = req.body; + body: { + secretValueCiphertext, + secretValueTag, + secretValueIV, + type, + environment, + secretPath, + workspaceId + }, + params: { secretName } + } = await validateRequest(reqValidator.UpdateSecretByNameV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const secret = await SecretService.updateSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -452,12 +526,22 @@ export const updateSecretByName = async (req: Request, res: Response) => { * @param res */ export const deleteSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretPath = "/" } = req.body; + const { + body: { type, environment, secretPath, workspaceId }, + params: { secretName } + } = await validateRequest(reqValidator.DeleteSecretByNameV3, req); + + if (req.user._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const { secret } = await SecretService.deleteSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index 449df4e11..aa466cd11 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -3,9 +3,7 @@ import { Request, Response } from "express"; import * as Sentry from "@sentry/node"; import { MembershipOrg, User } from "../../models"; import { completeAccount } from "../../helpers/user"; -import { - initializeDefaultOrg, -} from "../../helpers/signup"; +import { initializeDefaultOrg } from "../../helpers/signup"; import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; import { ACCEPTED, INVITED } from "../../variables"; import { standardRequest } from "../../config/request"; @@ -13,6 +11,8 @@ import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../confi import { BadRequestError } from "../../utils/errors"; import { TelemetryService } from "../../services"; import { AuthMethod } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; /** * Complete setting up user by adding their personal and auth information as part of the @@ -22,177 +22,174 @@ import { AuthMethod } from "../../models"; * @returns */ export const completeAccountSignup = async (req: Request, res: Response) => { - let user, token, refreshToken; - try { - const { - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - organizationName, - providerAuthToken, - attributionSource, - }: { - email: string; - firstName: string; - lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; - organizationName: string; - providerAuthToken?: string; - attributionSource?: string; - } = req.body; + let user, token; + try { + const { + body: { + email, + publicKey, + salt, + lastName, + verifier, + firstName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + organizationName, + providerAuthToken, + attributionSource, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag + } + } = await validateRequest(reqValidator.CompletedAccountSignupV3, req); - user = await User.findOne({ email }); + user = await User.findOne({ email }); - if (!user || (user && user?.publicKey)) { - // case 1: user doesn't exist. - // case 2: user has already completed account - return res.status(403).send({ - error: "Failed to complete account for complete user", - }); - } + if (!user || (user && user?.publicKey)) { + // case 1: user doesn't exist. + // case 2: user has already completed account + return res.status(403).send({ + error: "Failed to complete account for complete user" + }); + } - if (providerAuthToken) { - await validateProviderAuthToken({ - email, - providerAuthToken - }); - } else { - const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] - if (AUTH_TOKEN_TYPE === null) { - throw BadRequestError({ message: "Missing Authorization Header in the request header." }); - } - if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { - throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }) - } - if (AUTH_TOKEN_VALUE === null) { - throw BadRequestError({ - message: "Missing Authorization Body in the request header", - }) - } + if (providerAuthToken) { + await validateProviderAuthToken({ + email, + providerAuthToken, + user + }); + } else { + const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>( + req.headers["authorization"]?.split(" ", 2) + ) ?? [null, null]; + if (AUTH_TOKEN_TYPE === null) { + throw BadRequestError({ message: "Missing Authorization Header in the request header." }); + } + if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { + throw BadRequestError({ + message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` + }); + } + if (AUTH_TOKEN_VALUE === null) { + throw BadRequestError({ + message: "Missing Authorization Body in the request header" + }); + } - const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) - ); + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) + ); - if (decodedToken.userId !== user.id) { - throw BadRequestError(); - } - } + if (decodedToken.userId !== user.id) { + throw BadRequestError(); + } + } - // complete setting up user's account - user = await completeAccount({ - userId: user._id.toString(), - firstName, - lastName, - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - }); + // complete setting up user's account + user = await completeAccount({ + userId: user._id.toString(), + firstName, + lastName, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }); - if (!user) - throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null + if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null - const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)); - - if (!hasSamlEnabled) { // TODO: modify this part - // initialize default organization and workspace - await initializeDefaultOrg({ - organizationName, - user, - }); - } + const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => + [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod) + ); - // update organization membership statuses that are - // invited to completed with user attached - await MembershipOrg.updateMany( - { - inviteEmail: email, - status: INVITED, - }, - { - user, - status: ACCEPTED, - } - ); + if (!hasSamlEnabled) { + // TODO: modify this part + // initialize default organization and workspace + await initializeDefaultOrg({ + organizationName, + user + }); + } - // issue tokens - const tokens = await issueAuthTokens({ - userId: user._id, - ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - }); + // update organization membership statuses that are + // invited to completed with user attached + await MembershipOrg.updateMany( + { + inviteEmail: email, + status: INVITED + }, + { + user, + status: ACCEPTED + } + ); - token = tokens.token; + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); - // sending a welcome email to new users - if (await getLoopsApiKey()) { - await standardRequest.post("https://app.loops.so/api/v1/events/send", { - "email": email, - "eventName": "Sign Up", - "firstName": firstName, - "lastName": lastName, - }, { - headers: { - "Accept": "application/json", - "Authorization": "Bearer " + (await getLoopsApiKey()), - }, - }); - } + token = tokens.token; - // store (refresh) token in httpOnly cookie - res.cookie("jid", tokens.refreshToken, { - httpOnly: true, - path: "/", - sameSite: "strict", - secure: await getHttpsEnabled(), - }); + // sending a welcome email to new users + if (await getLoopsApiKey()) { + await standardRequest.post( + "https://app.loops.so/api/v1/events/send", + { + email: email, + eventName: "Sign Up", + firstName: firstName, + lastName: lastName + }, + { + headers: { + Accept: "application/json", + Authorization: "Bearer " + (await getLoopsApiKey()) + } + } + ); + } - const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: "User Signed Up", - distinctId: email, - properties: { - email, - ...(attributionSource ? { attributionSource } : {}) - }, - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to complete account setup", - }); - } + // store (refresh) token in httpOnly cookie + res.cookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled() + }); - return res.status(200).send({ - message: "Successfully set up account", - user, - token, - }); + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "User Signed Up", + distinctId: email, + properties: { + email, + ...(attributionSource ? { attributionSource } : {}) + } + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to complete account setup" + }); + } + + return res.status(200).send({ + message: "Successfully set up account", + user, + token + }); }; diff --git a/backend/src/controllers/v3/workspacesController.ts b/backend/src/controllers/v3/workspacesController.ts index 4298adba2..8681f3715 100644 --- a/backend/src/controllers/v3/workspacesController.ts +++ b/backend/src/controllers/v3/workspacesController.ts @@ -1,90 +1,103 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; +import { validateRequest } from "../../helpers/validation"; import { Secret } from "../../models"; -import { SecretService } from"../../services"; +import { SecretService } from "../../services"; +import { getUserProjectPermissions } from "../../services/ProjectRoleService"; +import { UnauthorizedRequestError } from "../../utils/errors"; +import * as reqValidator from "../../validation/workspace"; /** * Return whether or not all secrets in workspace with id [workspaceId] * are blind-indexed - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req); - const secretsWithoutBlindIndex = await Secret.countDocuments({ - workspace: new Types.ObjectId(workspaceId), - secretBlindIndex: { - $exists: false, - }, - }); + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); - return res.status(200).send(secretsWithoutBlindIndex === 0); -} + const secretsWithoutBlindIndex = await Secret.countDocuments({ + workspace: new Types.ObjectId(workspaceId), + secretBlindIndex: { + $exists: false + } + }); + + return res.status(200).send(secretsWithoutBlindIndex === 0); +}; /** * Get all secrets for workspace with id [workspaceId] */ export const getWorkspaceSecrets = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req); - const secrets = await Secret.find({ - workspace: new Types.ObjectId (workspaceId), - }); - - return res.status(200).send({ - secrets, - }); -} + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); + + const secrets = await Secret.find({ + workspace: new Types.ObjectId(workspaceId) + }); + + return res.status(200).send({ + secrets + }); +}; /** * Update blind indices for secrets in workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { - interface SecretToUpdate { - secretName: string; - _id: string; - } + const { + params: { workspaceId }, + body: { secretsToUpdate } + } = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req); - const { workspaceId } = req.params; - const { - secretsToUpdate, - }: { - secretsToUpdate: SecretToUpdate[]; - } = req.body; + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); - // get secret blind index salt - const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId), - }); + // get secret blind index salt + const salt = await SecretService.getSecretBlindIndexSalt({ + workspaceId: new Types.ObjectId(workspaceId) + }); - // update secret blind indices - const operations = await Promise.all( - secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => { - const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ - secretName: secretToUpdate.secretName, - salt, - }); - - return ({ - updateOne: { - filter: { - _id: new Types.ObjectId(secretToUpdate._id), - }, - update: { - secretBlindIndex, - }, - }, - }); - }) - ); + // update secret blind indices + const operations = await Promise.all( + secretsToUpdate.map(async (secretToUpdate) => { + const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ + secretName: secretToUpdate.secretName, + salt + }); - await Secret.bulkWrite(operations); + return { + updateOne: { + filter: { + _id: new Types.ObjectId(secretToUpdate._id) + }, + update: { + secretBlindIndex + } + } + }; + }) + ); - return res.status(200).send({ - message: "Successfully named workspace secrets", - }); -} \ No newline at end of file + await Secret.bulkWrite(operations); + + return res.status(200).send({ + message: "Successfully named workspace secrets" + }); +}; diff --git a/backend/src/ee/controllers/v1/actionController.ts b/backend/src/ee/controllers/v1/actionController.ts index 484c25351..56b076026 100644 --- a/backend/src/ee/controllers/v1/actionController.ts +++ b/backend/src/ee/controllers/v1/actionController.ts @@ -1,30 +1,32 @@ import { Request, Response } from "express"; import { Action } from "../../models"; import { ActionNotFoundError } from "../../../utils/errors"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/action"; export const getAction = async (req: Request, res: Response) => { - let action; - try { - const { actionId } = req.params; - - action = await Action - .findById(actionId) - .populate([ - "payload.secretVersions.oldSecretVersion", - "payload.secretVersions.newSecretVersion", - ]); - - if (!action) throw ActionNotFoundError({ - message: "Failed to find action", - }); + let action; + try { + const { + params: { actionId } + } = await validateRequest(reqValidator.GetActionV1, req); - } catch (err) { - throw ActionNotFoundError({ - message: "Failed to find action", - }); - } - - return res.status(200).send({ - action, + action = await Action.findById(actionId).populate([ + "payload.secretVersions.oldSecretVersion", + "payload.secretVersions.newSecretVersion" + ]); + + if (!action) + throw ActionNotFoundError({ + message: "Failed to find action" + }); + } catch (err) { + throw ActionNotFoundError({ + message: "Failed to find action" }); -} + } + + return res.status(200).send({ + action + }); +}; diff --git a/backend/src/ee/controllers/v1/cloudProductsController.ts b/backend/src/ee/controllers/v1/cloudProductsController.ts index e66fc66e5..0cc6ab372 100644 --- a/backend/src/ee/controllers/v1/cloudProductsController.ts +++ b/backend/src/ee/controllers/v1/cloudProductsController.ts @@ -2,27 +2,31 @@ import { Request, Response } from "express"; import { EELicenseService } from "../../services"; import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/cloudProducts"; /** * Return available cloud product information. * Note: Nicely formatted to easily construct a table from - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getCloudProducts = async (req: Request, res: Response) => { - const billingCycle = req.query["billing-cycle"] as string; + const { + query: { "billing-cycle": billingCycle } + } = await validateRequest(reqValidator.GetCloudProductsV1, req); - if (EELicenseService.instanceType === "cloud") { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` - ); + if (EELicenseService.instanceType === "cloud") { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); - return res.status(200).send(data); - } - - return res.status(200).send({ - head: [], - rows: [], - }); -} + return res.status(200).send(data); + } + + return res.status(200).send({ + head: [], + rows: [] + }); +}; diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 2dcc3c2c7..1203f406d 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,5 +1,14 @@ +import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; import { Secret } from "../../../models"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../../services/ProjectRoleService"; +import { BadRequestError } from "../../../utils/errors"; +import * as reqValidator from "../../../validation"; import { SecretVersion } from "../../models"; import { EESecretService } from "../../services"; @@ -54,10 +63,21 @@ export const getSecretVersions = async (req: Request, res: Response) => { } } */ - const { secretId } = req.params; + const { + params: { secretId }, + query: { offset, limit } + } = await validateRequest(reqValidator.GetSecretVersionsV1, req); - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); + const secret = await Secret.findById(secretId); + if (!secret) { + throw BadRequestError({ message: "Failed to find secret" }); + } + + const { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); const secretVersions = await SecretVersion.find({ secret: secretId @@ -126,8 +146,28 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { } } */ - const { secretId } = req.params; - const { version } = req.body; + + const { + params: { secretId }, + body: { version } + } = await validateRequest(reqValidator.RollbackSecretVersionV1, req); + + const toBeUpdatedSec = await Secret.findById(secretId); + if (!toBeUpdatedSec) { + throw BadRequestError({ message: "Failed to find secret" }); + } + const { permission } = await getUserProjectPermissions( + req.user._id, + toBeUpdatedSec.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRollback + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment }) + ); // validate secret version const oldSecretVersion = await SecretVersion.findOne({ diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index c10188b10..f2500d199 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -1,9 +1,13 @@ +import { ForbiddenError } from "@casl/ability"; import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; import { - ISecretVersion, - SecretSnapshot, - TFolderRootVersionSchema, -} from "../../models"; + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../../services/ProjectRoleService"; +import * as reqValidator from "../../../validation/secretSnapshot"; +import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models"; /** * Return secret snapshot with id [secretSnapshotId] @@ -12,7 +16,9 @@ import { * @returns */ export const getSecretSnapshot = async (req: Request, res: Response) => { - const { secretSnapshotId } = req.params; + const { + params: { secretSnapshotId } + } = await validateRequest(reqValidator.GetSecretSnapshotV1, req); const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) .lean() @@ -20,26 +26,36 @@ export const getSecretSnapshot = async (req: Request, res: Response) => { path: "secretVersions", populate: { path: "tags", - model: "Tag", - }, + model: "Tag" + } }) .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); - + if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - + + const { permission } = await getUserProjectPermissions( + req.user._id, + secretSnapshot.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); + const folderId = secretSnapshot.folderId; // to show only the folder required secrets secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter( ({ folder }) => folder === folderId ); - secretSnapshot.folderVersion = - secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({ + secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map( + ({ id, name }) => ({ id, - name, - })) as any; + name + }) + ) as any; return res.status(200).send({ - secretSnapshot, + secretSnapshot }); }; diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 1e21dd591..42c68b9fe 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -22,16 +22,32 @@ import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { searchByFolderId } from "../../../services/FolderService"; import { EEAuditLogService, EELicenseService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; +import { validateRequest } from "../../../helpers/validation"; +import { + AddWorkspaceTrustedIpV1, + DeleteWorkspaceTrustedIpV1, + GetWorkspaceAuditLogActorFilterOptsV1, + GetWorkspaceAuditLogsV1, + GetWorkspaceLogsV1, + GetWorkspaceSecretSnapshotsCountV1, + GetWorkspaceSecretSnapshotsV1, + GetWorkspaceTrustedIpsV1, + RollbackWorkspaceSecretSnapshotV1, + UpdateWorkspaceTrustedIpV1 +} from "../../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Return secret snapshots for workspace with id [workspaceId] * @param req * @param res */ -export const getWorkspaceSecretSnapshots = async ( - req: Request, - res: Response -) => { +export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { /* #swagger.summary = 'Return project secret snapshot ids' #swagger.description = 'Return project secret snapshots ids' @@ -77,23 +93,28 @@ export const getWorkspaceSecretSnapshots = async ( } } */ - const { workspaceId } = req.params; - const { environment, folderId } = req.query; + const { + params: { workspaceId }, + query: { environment, folderId, offset, limit } + } = await validateRequest(GetWorkspaceSecretSnapshotsV1, req); - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); const secretSnapshots = await SecretSnapshot.find({ workspace: workspaceId, environment, - folderId: folderId || "root", + folderId: folderId || "root" }) .sort({ createdAt: -1 }) .skip(offset) .limit(limit); return res.status(200).send({ - secretSnapshots, + secretSnapshots }); }; @@ -102,21 +123,26 @@ export const getWorkspaceSecretSnapshots = async ( * @param req * @param res */ -export const getWorkspaceSecretSnapshotsCount = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environment, folderId } = req.query; +export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + query: { environment, folderId } + } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); const count = await SecretSnapshot.countDocuments({ workspace: workspaceId, environment, - folderId: folderId || "root", + folderId: folderId || "root" }); return res.status(200).send({ - count, + count }); }; @@ -126,10 +152,7 @@ export const getWorkspaceSecretSnapshotsCount = async ( * @param res * @returns */ -export const rollbackWorkspaceSecretSnapshot = async ( - req: Request, - res: Response -) => { +export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => { /* #swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.' #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.' @@ -181,19 +204,27 @@ export const rollbackWorkspaceSecretSnapshot = async ( } */ - const { workspaceId } = req.params; - const { version, environment, folderId = "root" } = req.body; + const { + params: { workspaceId }, + body: { folderId, environment, version } + } = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRollback + ); // validate secret snapshot const secretSnapshot = await SecretSnapshot.findOne({ workspace: workspaceId, version, environment, - folderId: folderId, + folderId: folderId }) .populate<{ secretVersions: ISecretVersion[] }>({ path: "secretVersions", - select: "+secretBlindIndex", + select: "+secretBlindIndex" }) .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); @@ -202,13 +233,13 @@ export const rollbackWorkspaceSecretSnapshot = async ( const snapshotFolderTree = secretSnapshot.folderVersion; const latestFolderTree = await Folder.findOne({ workspace: workspaceId, - environment, + environment }); const latestFolderVersion = await FolderVersion.findOne({ environment, workspace: workspaceId, - "nodes.id": folderId, + "nodes.id": folderId }).sort({ "nodes.version": -1 }); const oldSecretVersionsObj: Record = {}; @@ -222,8 +253,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( // the parent node from current latest one // this will be modified according to the snapshot and latest snapshots - const newFolderTree = - latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); + const newFolderTree = latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); if (newFolderTree) { newFolderTree.children = snapshotFolderTree?.nodes?.children || []; @@ -252,43 +282,43 @@ export const rollbackWorkspaceSecretSnapshot = async ( workspace: new Types.ObjectId(workspaceId), environment, folderId: { - $in: Object.keys(groupByFolderId), - }, - }, + $in: Object.keys(groupByFolderId) + } + } }; const sortByFolderIdAndVersion: PipelineStage = { - $sort: { folderId: 1, version: -1 }, + $sort: { folderId: 1, version: -1 } }; const pickLatestVersionOfEachFolder = { $group: { _id: "$folderId", latestVersion: { $first: "$version" }, doc: { - $first: "$$ROOT", - }, - }, + $first: "$$ROOT" + } + } }; const populateSecVersion = { $lookup: { from: SecretVersion.collection.name, localField: "doc.secretVersions", foreignField: "_id", - as: "doc.secretVersions", - }, + as: "doc.secretVersions" + } }; const populateFolderVersion = { $lookup: { from: FolderVersion.collection.name, localField: "doc.folderVersion", foreignField: "_id", - as: "doc.folderVersion", - }, + as: "doc.folderVersion" + } }; const unwindFolderVerField = { $unwind: { path: "$doc.folderVersion", - preserveNullAndEmptyArrays: true, - }, + preserveNullAndEmptyArrays: true + } }; const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> = await SecretSnapshot.aggregate([ @@ -297,7 +327,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( pickLatestVersionOfEachFolder, populateSecVersion, populateFolderVersion, - unwindFolderVerField, + unwindFolderVerField ]); // recursive snapshotting each level @@ -327,7 +357,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( // TODO: fix any const latestSecretVersionIds = await getLatestSecretVersionIds({ - secretIds, + secretIds }); // TODO: fix any @@ -335,32 +365,31 @@ export const rollbackWorkspaceSecretSnapshot = async ( await SecretVersion.find( { _id: { - $in: latestSecretVersionIds.map((s) => s.versionId), - }, + $in: latestSecretVersionIds.map((s) => s.versionId) + } }, "secret version" ) ).reduce( (accumulator, s) => ({ ...accumulator, - [`${s.secret.toString()}`]: s, + [`${s.secret.toString()}`]: s }), {} ); const secDelQuery: Record = { workspace: workspaceId, - environment, + environment // undefined means root thus collect all secrets }; - if (folderId !== "root" && folderIds.length) - secDelQuery.folder = { $in: folderIds }; + if (folderId !== "root" && folderIds.length) secDelQuery.folder = { $in: folderIds }; // delete existing secrets await Secret.deleteMany(secDelQuery); await Folder.deleteOne({ workspace: workspaceId, - environment, + environment }); // add secrets @@ -382,7 +411,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( createdAt, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId } = oldSecretVersionsObj[sv]; return { @@ -405,7 +434,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( createdAt, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId }; }) ); @@ -429,7 +458,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretValueTag, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId }) => ({ _id: new Types.ObjectId(), secret: _id, @@ -448,7 +477,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretValueTag, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId }) ) ); @@ -464,7 +493,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( const newFolderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: newFolderTree, + nodes: newFolderTree }); await newFolderVersion.save(); } @@ -473,13 +502,11 @@ export const rollbackWorkspaceSecretSnapshot = async ( await SecretVersion.updateMany( { secret: { - $in: Object.keys(oldSecretVersionsObj).map( - (sv) => oldSecretVersionsObj[sv].secret - ), - }, + $in: Object.keys(oldSecretVersionsObj).map((sv) => oldSecretVersionsObj[sv].secret) + } }, { - isDeleted: false, + isDeleted: false } ); @@ -487,11 +514,11 @@ export const rollbackWorkspaceSecretSnapshot = async ( await EESecretService.takeSecretSnapshot({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, + folderId }); return res.status(200).send({ - secrets, + secrets }); }; @@ -568,13 +595,16 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { } } */ - const { workspaceId } = req.params; + const { + query: { limit, offset, userId, sortBy, actionNames }, + params: { workspaceId } + } = await validateRequest(GetWorkspaceLogsV1, req); - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - const sortBy: string = req.query.sortBy as string; - const userId: string = req.query.userId as string; - const actionNames: string = req.query.actionNames as string; + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); const logs = await Log.find({ workspace: workspaceId, @@ -582,10 +612,10 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { ...(actionNames ? { actionNames: { - $in: actionNames.split(","), - }, + $in: actionNames.split(",") + } } - : {}), + : {}) }) .sort({ createdAt: sortBy === "recent" ? -1 : 1 }) .skip(offset) @@ -594,93 +624,109 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { .populate("user serviceAccount serviceTokenData"); return res.status(200).send({ - logs, + logs }); }; /** * Return audit logs for workspace with id [workspaceId] * @param req - * @param res + * @param res */ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const eventType = req.query.eventType; - const userAgentType = req.query.userAgentType; - const actor = req.query.actor as string | undefined; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - - const startDate = req.query.startDate as string; - const endDate = req.query.endDate as string; - + const { + query: { limit, offset, endDate, eventType, startDate, userAgentType, actor }, + params: { workspaceId } + } = await validateRequest(GetWorkspaceAuditLogsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); + const query = { workspace: new Types.ObjectId(workspaceId), - ...(eventType ? { - "event.type": eventType - } : {}), - ...(userAgentType ? { - userAgentType - } : {}), - ...(actor ? { - "actor.type": actor.split("-", 2)[0], - ...(actor.split("-", 2)[0] === ActorType.USER ? { - "actor.metadata.userId": actor.split("-", 2)[1] - } : { - "actor.metadata.serviceId": actor.split("-", 2)[1] - }) - } : {}), - ...(startDate || endDate ? { - createdAt: { - ...(startDate && { $gte: new Date(startDate) }), - ...(endDate && { $lte: new Date(endDate) }) - } - } : {}) - } - - const auditLogs = await AuditLog.find(query) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); + ...(eventType + ? { + "event.type": eventType + } + : {}), + ...(userAgentType + ? { + userAgentType + } + : {}), + ...(actor + ? { + "actor.type": actor.split("-", 2)[0], + ...(actor.split("-", 2)[0] === ActorType.USER + ? { + "actor.metadata.userId": actor.split("-", 2)[1] + } + : { + "actor.metadata.serviceId": actor.split("-", 2)[1] + }) + } + : {}), + ...(startDate || endDate + ? { + createdAt: { + ...(startDate && { $gte: new Date(startDate) }), + ...(endDate && { $lte: new Date(endDate) }) + } + } + : {}) + }; + + const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit); const totalCount = await AuditLog.countDocuments(query); - + return res.status(200).send({ auditLogs, totalCount }); -} +}; /** * Return audit log actor filter options for workspace with id [workspaceId] * @param req - * @param res + * @param res */ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - + const { + params: { workspaceId } + } = await validateRequest(GetWorkspaceAuditLogActorFilterOptsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); + const userIds = await Membership.distinct("user", { workspace: new Types.ObjectId(workspaceId) }); - const userActors: UserActor[] = (await User.find({ - _id: { - $in: userIds - } - }) - .select("email")) - .map((user) => ({ + const userActors: UserActor[] = ( + await User.find({ + _id: { + $in: userIds + } + }).select("email") + ).map((user) => ({ type: ActorType.USER, metadata: { userId: user._id.toString(), email: user.email } })); - - const serviceActors: ServiceActor[] = (await ServiceTokenData.find({ - workspace: new Types.ObjectId(workspaceId) - }) - .select("name")) - .map((serviceTokenData) => ({ + + const serviceActors: ServiceActor[] = ( + await ServiceTokenData.find({ + workspace: new Types.ObjectId(workspaceId) + }).select("name") + ).map((serviceTokenData) => ({ type: ActorType.SERVICE, metadata: { serviceId: serviceTokenData._id.toString(), @@ -691,50 +737,65 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res return res.status(200).send({ actors: [...userActors, ...serviceActors] }); -} +}; /** * Return trusted ips for workspace with id [workspaceId] * @param req - * @param res + * @param res */ export const getWorkspaceTrustedIps = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(GetWorkspaceTrustedIpsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.IpAllowList + ); const trustedIps = await TrustedIP.find({ workspace: new Types.ObjectId(workspaceId) }); - + return res.status(200).send({ trustedIps }); -} +}; /** * Add a trusted ip to workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { - const { workspaceId } = req.params; const { - ipAddress: ip, - comment, - isActive - } = req.body; - + params: { workspaceId }, + body: { comment, isActive, ipAddress: ip } + } = await validateRequest(AddWorkspaceTrustedIpV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.IpAllowList + ); + const plan = await EELicenseService.getPlan(req.workspace.organization); - - if (!plan.ipAllowlisting) return res.status(400).send({ - message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range." - }); - + + if (!plan.ipAllowlisting) + return res.status(400).send({ + message: + "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range." + }); + const isValidIPOrCidr = isValidIpOrCidr(ip); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - + + if (!isValidIPOrCidr) + return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + const { ipAddress, type, prefix } = extractIPDetails(ip); const trustedIp = await new TrustedIP({ @@ -743,9 +804,9 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { type, prefix, isActive, - comment, + comment }).save(); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -764,32 +825,40 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { return res.status(200).send({ trustedIp }); -} +}; /** * Update trusted ip with id [trustedIpId] workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { - const { workspaceId, trustedIpId } = req.params; const { - ipAddress: ip, - comment - } = req.body; + params: { workspaceId, trustedIpId }, + body: { ipAddress: ip, comment } + } = await validateRequest(UpdateWorkspaceTrustedIpV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.IpAllowList + ); const plan = await EELicenseService.getPlan(req.workspace.organization); - if (!plan.ipAllowlisting) return res.status(400).send({ - message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range." - }); + if (!plan.ipAllowlisting) + return res.status(400).send({ + message: + "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range." + }); const isValidIPOrCidr = isValidIpOrCidr(ip); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - + + if (!isValidIPOrCidr) + return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + const { ipAddress, type, prefix } = extractIPDetails(ip); const updateObject: { @@ -799,33 +868,34 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { prefix?: number; $unset?: { prefix: number; - } + }; } = { ipAddress, type, comment }; - + if (prefix !== undefined) { updateObject.prefix = prefix; } else { updateObject.$unset = { prefix: 1 }; } - + const trustedIp = await TrustedIP.findOneAndUpdate( { _id: new Types.ObjectId(trustedIpId), - workspace: new Types.ObjectId(workspaceId), + workspace: new Types.ObjectId(workspaceId) }, updateObject, { new: true } ); - - if (!trustedIp) return res.status(400).send({ - message: "Failed to update trusted IP" - }); + + if (!trustedIp) + return res.status(400).send({ + message: "Failed to update trusted IP" + }); await EEAuditLogService.createAuditLog( req.authData, @@ -841,34 +911,45 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { workspaceId: trustedIp.workspace } ); - + return res.status(200).send({ trustedIp }); -} +}; /** * Delete IP access range from workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { - const { workspaceId, trustedIpId } = req.params; + const { + params: { workspaceId, trustedIpId } + } = await validateRequest(DeleteWorkspaceTrustedIpV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.IpAllowList + ); const plan = await EELicenseService.getPlan(req.workspace.organization); - - if (!plan.ipAllowlisting) return res.status(400).send({ - message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range." - }); - + + if (!plan.ipAllowlisting) + return res.status(400).send({ + message: + "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range." + }); + const trustedIp = await TrustedIP.findOneAndDelete({ _id: new Types.ObjectId(trustedIpId), workspace: new Types.ObjectId(workspaceId) }); - - if (!trustedIp) return res.status(400).send({ - message: "Failed to delete trusted IP" - }); + + if (!trustedIp) + return res.status(400).send({ + message: "Failed to delete trusted IP" + }); await EEAuditLogService.createAuditLog( req.authData, @@ -888,4 +969,4 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { return res.status(200).send({ trustedIp }); -} \ No newline at end of file +}; diff --git a/backend/src/ee/routes/v1/action.ts b/backend/src/ee/routes/v1/action.ts index b77a84188..4700c5a59 100644 --- a/backend/src/ee/routes/v1/action.ts +++ b/backend/src/ee/routes/v1/action.ts @@ -1,17 +1,8 @@ import express from "express"; const router = express.Router(); -import { - validateRequest, -} from "../../../middleware"; -import { param } from "express-validator"; import { actionController } from "../../controllers/v1"; // TODO: put into action controller -router.get( - "/:actionId", - param("actionId").exists().trim(), - validateRequest, - actionController.getAction -); +router.get("/:actionId", actionController.getAction); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/cloudProducts.ts b/backend/src/ee/routes/v1/cloudProducts.ts index 81256f378..23912222b 100644 --- a/backend/src/ee/routes/v1/cloudProducts.ts +++ b/backend/src/ee/routes/v1/cloudProducts.ts @@ -1,21 +1,16 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - validateRequest, -} from "../../../middleware"; -import { query } from "express-validator"; +import { requireAuth, validateRequest } from "../../../middleware"; import { cloudProductsController } from "../../controllers/v1"; import { AuthMode } from "../../../variables"; router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - query("billing-cycle").exists().isIn(["monthly", "yearly"]), - validateRequest, - cloudProductsController.getCloudProducts + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + validateRequest, + cloudProductsController.getCloudProducts ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 0eb23ee80..bdbdec965 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -1,48 +1,25 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireSecretAuth, - validateRequest, -} from "../../../middleware"; -import { body, param, query } from "express-validator"; +import { requireAuth } from "../../../middleware"; import { secretController } from "../../controllers/v1"; import { - ADMIN, - AuthMode, - MEMBER, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS + AuthMode } from "../../../variables"; router.get( - "/:secretId/secret-versions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS], - }), - param("secretId").exists().trim(), - query("offset").exists().isInt(), - query("limit").exists().isInt(), - validateRequest, - secretController.getSecretVersions + "/:secretId/secret-versions", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + secretController.getSecretVersions ); router.post( - "/:secretId/secret-versions/rollback", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS], - }), - param("secretId").exists().trim(), - body("version").exists().isInt(), - secretController.rollbackSecretVersion + "/:secretId/secret-versions/rollback", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + secretController.rollbackSecretVersion ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 529ba0550..9a7054ce6 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -1,182 +1,85 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../../middleware"; -import { body, param, query } from "express-validator"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; import { workspaceController } from "../../controllers/v1"; -import { EventType, UserAgentType } from "../../models"; router.get( "/:workspaceId/secret-snapshots", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("environment").isString().exists().trim(), - query("folderId").default("root").isString().trim(), - query("offset").exists().isInt(), - query("limit").exists().isInt(), - validateRequest, workspaceController.getWorkspaceSecretSnapshots ); router.get( "/:workspaceId/secret-snapshots/count", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("environment").isString().exists().trim(), - query("folderId").default("root").isString().trim(), - validateRequest, workspaceController.getWorkspaceSecretSnapshotsCount ); router.post( "/:workspaceId/secret-snapshots/rollback", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environment").isString().exists().trim(), - query("folderId").default("root").isString().exists().trim(), - body("version").exists().isInt(), - validateRequest, workspaceController.rollbackWorkspaceSecretSnapshot ); router.get( "/:workspaceId/logs", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("offset").exists().isInt(), - query("limit").exists().isInt(), - query("sortBy"), - query("userId"), - query("actionNames"), - validateRequest, workspaceController.getWorkspaceLogs ); router.get( "/:workspaceId/audit-logs", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), - query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), - query("actor").optional({ nullable: true }), - query("startDate").isISO8601().withMessage("Invalid start date format").optional({ nullable: true }), - query("endDate").isISO8601().withMessage("Invalid end date format").optional({ nullable: true }), - query("offset"), - query("limit"), - validateRequest, workspaceController.getWorkspaceAuditLogs ); router.get( "/:workspaceId/audit-logs/filters/actors", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, workspaceController.getWorkspaceAuditLogActorFilterOpts ); router.get( "/:workspaceId/trusted-ips", - param("workspaceId").exists().isString().trim(), requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.getWorkspaceTrustedIps ); router.post( "/:workspaceId/trusted-ips", - param("workspaceId").exists().isString().trim(), - body("ipAddress").exists().isString().trim(), - body("comment").default("").isString().trim(), - body("isActive").exists().isBoolean(), - validateRequest, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.addWorkspaceTrustedIp ); router.patch( "/:workspaceId/trusted-ips/:trustedIpId", - param("workspaceId").exists().isString().trim(), - param("trustedIpId").exists().isString().trim(), - body("ipAddress").isString().trim().default(""), - body("comment").default("").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.updateWorkspaceTrustedIp ); router.delete( "/:workspaceId/trusted-ips/:trustedIpId", - param("workspaceId").exists().isString().trim(), - param("trustedIpId").exists().isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.deleteWorkspaceTrustedIp ); diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 485c19d7e..2775d79c6 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -160,7 +160,7 @@ export const getIntegrationAuthAccessHelper = async ({ let accessId; let accessToken; const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( - "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag" + "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag" ); if (!integrationAuth) @@ -209,6 +209,7 @@ export const getIntegrationAuthAccessHelper = async ({ if (!accessToken) throw InternalServerError(); return { + integrationAuth, accessId, accessToken }; diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index d9c0d6120..42a59ffb1 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,6 +1,5 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { authController } from "../../controllers/v1"; import { authLimiter } from "../../helpers/rateLimiter"; @@ -12,9 +11,6 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, - body("email").exists().trim().notEmpty().toLowerCase(), - body("clientPublicKey").exists().trim().notEmpty(), - validateRequest, authController.login1 ); @@ -22,9 +18,6 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2) "/login2", authLimiter, - body("email").exists().trim().notEmpty().toLowerCase(), - body("clientProof").exists().trim().notEmpty(), - validateRequest, authController.login2 ); @@ -45,6 +38,8 @@ router.post( authController.checkAuth ); +router.get("/common-passwords", authLimiter, authController.getCommonPasswords); + router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) "/sessions", diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index e50d35625..5e76288cd 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -1,41 +1,25 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; import { - requireAuth, - requireBotAuth, - requireWorkspaceAuth, - validateRequest, + requireAuth } from "../../middleware"; import { botController } from "../../controllers/v1"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { AuthMode } from "../../variables"; router.get( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim().notEmpty(), - validateRequest, - botController.getBotByWorkspaceId + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + botController.getBotByWorkspaceId ); router.patch( - "/:botId/active", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireBotAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - body("isActive").exists().isBoolean(), - body("botKey"), - validateRequest, - botController.setBotActiveState + "/:botId/active", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + botController.setBotActiveState ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 22aa901ea..e97132238 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -1,45 +1,16 @@ import express from "express"; const router = express.Router(); import { - requireAuth, - requireIntegrationAuth, - requireIntegrationAuthorizationAuth, - requireWorkspaceAuth, - validateRequest, + requireAuth } from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; -import { body, param } from "express-validator"; +import { AuthMode } from "../../variables"; import { integrationController } from "../../controllers/v1"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - location: "body", - }), - body("integrationAuthId").exists().isString().trim(), - body("isActive").exists().isBoolean(), - body("app").optional().isString().trim(), - body("appId").optional().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("sourceEnvironment").trim(), - body("targetEnvironment").trim(), - body("targetEnvironmentId").trim(), - body("targetService").trim(), - body("targetServiceId").trim(), - body("owner").trim(), - body("path").trim(), - body("region").trim(), - body("metadata").optional().isObject().withMessage("Metadata should be an object"), - body("metadata.secretSuffix").optional().isString().withMessage("Suffix should be a string"), - validateRequest, integrationController.createIntegration ); @@ -48,18 +19,6 @@ router.patch( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationId").exists().trim(), - body("isActive").exists().isBoolean(), - body("app").exists().trim(), - body("secretPath").default("/").isString().trim(), - body("environment").exists().trim(), - body("appId").exists(), - body("targetEnvironment").exists(), - body("owner").exists(), - validateRequest, integrationController.updateIntegration ); @@ -68,11 +27,6 @@ router.delete( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationId").exists().trim(), - validateRequest, integrationController.deleteIntegration ); @@ -81,13 +35,6 @@ router.post( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - body("environment").isString().exists().trim(), - body("workspaceId").exists().trim(), - validateRequest, integrationController.manualSync ); diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 73278a962..9a260c538 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -1,174 +1,95 @@ import express from "express"; const router = express.Router(); -import { body, param, query } from "express-validator"; -import { - requireAuth, - requireIntegrationAuthorizationAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { integrationAuthController } from "../../controllers/v1"; router.get( - "/integration-options", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - integrationAuthController.getIntegrationOptions + "/integration-options", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationOptions ); router.get( - "/:integrationAuthId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - attachAccessToken: false - }), - param("integrationAuthId"), - validateRequest, - integrationAuthController.getIntegrationAuth + "/:integrationAuthId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuth ); router.post( - "/oauth-token", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - body("workspaceId").exists().trim().notEmpty(), - body("code").exists().trim().notEmpty(), - body("integration").exists().trim().notEmpty(), - body("url").optional().isString().trim(), - validateRequest, - integrationAuthController.oAuthExchange + "/oauth-token", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.oAuthExchange ); router.post( - "/access-token", - body("workspaceId").exists().trim().notEmpty(), - body("refreshToken").optional().isString().trim().notEmpty(), - body("accessId").optional().isString().trim(), - body("accessToken").optional().isString().trim().notEmpty(), - body("url").trim(), - body("namespace").trim(), - body("integration").exists().trim().notEmpty(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - integrationAuthController.saveIntegrationToken + "/access-token", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + integrationAuthController.saveIntegrationToken ); router.get( - "/:integrationAuthId/apps", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId"), - query("teamId"), - query("workspaceSlug"), - validateRequest, - integrationAuthController.getIntegrationAuthApps + "/:integrationAuthId/apps", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthApps ); router.get( - "/:integrationAuthId/teams", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId"), - validateRequest, - integrationAuthController.getIntegrationAuthTeams + "/:integrationAuthId/teams", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthTeams ); router.get( - "/:integrationAuthId/vercel/branches", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - query("teamId").optional().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthVercelBranches + "/:integrationAuthId/vercel/branches", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthVercelBranches ); router.get( - "/:integrationAuthId/railway/environments", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthRailwayEnvironments + "/:integrationAuthId/railway/environments", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthRailwayEnvironments ); router.get( - "/:integrationAuthId/railway/services", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthRailwayServices + "/:integrationAuthId/railway/services", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthRailwayServices ); router.get( - "/:integrationAuthId/bitbucket/workspaces", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthBitBucketWorkspaces + "/:integrationAuthId/bitbucket/workspaces", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthBitBucketWorkspaces ); router.get( - "/:integrationAuthId/northflank/secret-groups", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthNorthflankSecretGroups + "/:integrationAuthId/northflank/secret-groups", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthNorthflankSecretGroups ); router.get( @@ -176,27 +97,15 @@ router.get( requireAuth({ acceptedAuthModes: [AuthMode.JWT], }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, integrationAuthController.getIntegrationAuthTeamCityBuildConfigs ); router.delete( - "/:integrationAuthId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - attachAccessToken: false, - }), - param("integrationAuthId"), - validateRequest, - integrationAuthController.deleteIntegrationAuth + "/:integrationAuthId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.deleteIntegrationAuth ); export default router; diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index a72b508b9..be2c8d929 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -1,43 +1,26 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { body, param } from "express-validator"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { keyController } from "../../controllers/v1"; // TODO endpoint: consider moving these endpoints to be under /workspaces to be more RESTful router.post( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("key").exists(), - validateRequest, - keyController.uploadKey + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + keyController.uploadKey ); -router.get( // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something) - "/:workspaceId/latest", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId"), - validateRequest, - keyController.getLatestKey +router.get( + // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something) + "/:workspaceId/latest", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + keyController.getLatestKey ); export default router; diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index 54783e835..fc2070133 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -3,7 +3,6 @@ const router = express.Router(); import { body, param } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { membershipController } from "../../controllers/v1"; -import { membershipController as EEMembershipControllers } from "../../ee/controllers/v1"; import { AuthMode } from "../../variables"; // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) @@ -42,16 +41,4 @@ router.post( membershipController.changeMembershipRole ); -router.post( - // TODO endpoint: check dashboard - "/:membershipId/deny-permissions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - param("membershipId").isMongoId().exists().trim(), - body("permissions").isArray().exists(), - validateRequest, - EEMembershipControllers.denyMembershipPermissions -); - export default router; diff --git a/backend/src/routes/v1/password.ts b/backend/src/routes/v1/password.ts index 3bccc0934..aec995cee 100644 --- a/backend/src/routes/v1/password.ts +++ b/backend/src/routes/v1/password.ts @@ -1,93 +1,51 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; -import { requireAuth, requireSignupAuth, validateRequest } from "../../middleware"; +import { requireAuth, requireSignupAuth } from "../../middleware"; import { passwordController } from "../../controllers/v1"; import { passwordLimiter } from "../../helpers/rateLimiter"; import { AuthMode } from "../../variables"; router.post( - "/srp1", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("clientPublicKey").exists().isString().trim().notEmpty(), - validateRequest, - passwordController.srp1 + "/srp1", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + passwordController.srp1 ); router.post( - "/change-password", - passwordLimiter, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("clientProof").exists().trim().notEmpty(), - body("protectedKey").exists().isString().trim().notEmpty(), - body("protectedKeyIV").exists().isString().trim().notEmpty(), - body("protectedKeyTag").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // private key encrypted under new pwd - body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), // new iv for private key - body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), // new tag for private key - body("salt").exists().isString().trim().notEmpty(), // part of new pwd - body("verifier").exists().isString().trim().notEmpty(), // part of new pwd - validateRequest, - passwordController.changePassword + "/change-password", + passwordLimiter, + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + passwordController.changePassword ); -router.post( - "/email/password-reset", - passwordLimiter, - body("email").exists().isString().trim().notEmpty().isEmail(), - validateRequest, - passwordController.emailPasswordReset -); +router.post("/email/password-reset", passwordLimiter, passwordController.emailPasswordReset); router.post( - "/email/password-reset-verify", - passwordLimiter, - body("email").exists().isString().trim().notEmpty().isEmail(), - body("code").exists().isString().trim().notEmpty(), - validateRequest, - passwordController.emailPasswordResetVerify + "/email/password-reset-verify", + passwordLimiter, + passwordController.emailPasswordResetVerify ); router.get( - "/backup-private-key", - passwordLimiter, - requireSignupAuth, - passwordController.getBackupPrivateKey + "/backup-private-key", + passwordLimiter, + requireSignupAuth, + passwordController.getBackupPrivateKey ); router.post( - "/backup-private-key", - passwordLimiter, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("clientProof").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key - body("iv").exists().isString().trim().notEmpty(), // new iv for (backup) private key - body("tag").exists().isString().trim().notEmpty(), // new tag for (backup) private key - body("salt").exists().isString().trim().notEmpty(), // salt generated from strong key - body("verifier").exists().isString().trim().notEmpty(), // salt generated from strong key - validateRequest, - passwordController.createBackupPrivateKey + "/backup-private-key", + passwordLimiter, + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + passwordController.createBackupPrivateKey ); -router.post( - "/password-reset", - requireSignupAuth, - body("protectedKey").exists().isString().trim().notEmpty(), - body("protectedKeyIV").exists().isString().trim().notEmpty(), - body("protectedKeyTag").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // private key encrypted under new pwd - body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), // new iv for private key - body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), // new tag for private key - body("salt").exists().isString().trim().notEmpty(), // part of new pwd - body("verifier").exists().isString().trim().notEmpty(), // part of new pwd - validateRequest, - passwordController.resetPassword -); +router.post("/password-reset", requireSignupAuth, passwordController.resetPassword); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/secretImport.ts b/backend/src/routes/v1/secretImport.ts index 21973f57e..11232d970 100644 --- a/backend/src/routes/v1/secretImport.ts +++ b/backend/src/routes/v1/secretImport.ts @@ -1,83 +1,46 @@ import express from "express"; -import { body, param, query } from "express-validator"; import { secretImportController } from "../../controllers/v1"; -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; const router = express.Router(); router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body" - }), - body("workspaceId").exists().isString().trim().notEmpty(), - body("environment").exists().isString().trim().notEmpty(), - body("folderId").default("root").isString().trim(), - body("secretImport").exists().isObject(), - body("secretImport.environment").isString().exists().trim(), - body("secretImport.secretPath").isString().exists().trim(), - validateRequest, secretImportController.createSecretImport ); router.put( "/:id", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - param("id").exists().isString().trim(), - body("secretImports").exists().isArray(), - body("secretImports.*.environment").isString().exists().trim(), - body("secretImports.*.secretPath").isString().exists().trim(), - validateRequest, secretImportController.updateSecretImport ); router.delete( "/:id", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - param("id").exists().isString().trim(), - body("secretImportPath").isString().exists().trim(), - body("secretImportEnv").isString().exists().trim(), - validateRequest, secretImportController.deleteSecretImport ); router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query" - }), - query("workspaceId").exists().isString().trim().notEmpty(), - query("environment").exists().isString().trim().notEmpty(), - query("folderId").default("root").isString().trim(), - validateRequest, secretImportController.getSecretImports ); router.get( "/secrets", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query" - }), - query("workspaceId").exists().isString().trim().notEmpty(), - query("environment").exists().isString().trim().notEmpty(), - query("folderId").default("root").isString().trim(), - validateRequest, secretImportController.getAllSecretsFromImport ); diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 4580e50d3..edc2b09c5 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -1,66 +1,43 @@ import express from "express"; -import { body, param, query } from "express-validator"; import { createFolder, deleteFolder, getFolders, updateFolderById } from "../../controllers/v1/secretsFolderController"; -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; const router = express.Router(); router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body" - }), - body("workspaceId").exists(), - body("environment").exists(), - body("folderName").exists(), - body("parentFolderId"), - validateRequest, createFolder ); router.patch( "/:folderId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - body("workspaceId").exists(), - body("environment").exists(), - param("folderId").not().isIn(["root"]).exists(), - validateRequest, updateFolderById ); router.delete( "/:folderId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - body("workspaceId").exists(), - body("environment").exists(), - param("folderId").not().isIn(["root"]).exists(), - validateRequest, deleteFolder ); router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("parentFolderId").optional().isString().trim(), - query("parentFolderPath").optional().isString().trim(), - validateRequest, getFolders ); diff --git a/backend/src/routes/v1/signup.ts b/backend/src/routes/v1/signup.ts index 80d250b1a..f3b5d3adf 100644 --- a/backend/src/routes/v1/signup.ts +++ b/backend/src/routes/v1/signup.ts @@ -1,27 +1,21 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; -import { validateRequest } from "../../middleware"; import { signupController } from "../../controllers/v1"; import { authLimiter } from "../../helpers/rateLimiter"; // TODO: consider moving to users/v3/signup -router.post( // TODO endpoint: consider moving to v3/users/signup/mail - "/email/signup", - authLimiter, - body("email").exists().trim().notEmpty().isEmail(), - validateRequest, - signupController.beginEmailSignup +router.post( + // TODO endpoint: consider moving to v3/users/signup/mail + "/email/signup", + authLimiter, + signupController.beginEmailSignup ); router.post( - "/email/verify", // TODO endpoint: consider moving to v3/users/signup/verify - authLimiter, - body("email").exists().trim().notEmpty().isEmail(), - body("code").exists().trim().notEmpty(), - validateRequest, - signupController.verifyEmailSignup + "/email/verify", // TODO endpoint: consider moving to v3/users/signup/verify + authLimiter, + signupController.verifyEmailSignup ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index 7fd26f783..762e1cde3 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -1,29 +1,25 @@ import express from "express"; const router = express.Router(); -import { requireAuth, validateRequest } from "../../middleware"; -import { body, query } from "express-validator"; +import { requireAuth } from "../../middleware"; import { userActionController } from "../../controllers/v1"; import { AuthMode } from "../../variables"; // note: [userAction] will be deprecated in /v2 in favor of [action] -router.post( // TODO endpoint: move this into /users/me - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("action"), - validateRequest, - userActionController.addUserAction +router.post( + // TODO endpoint: move this into /users/me + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + userActionController.addUserAction ); router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - query("action"), - validateRequest, - userActionController.getUserAction + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + userActionController.getUserAction ); export default router; diff --git a/backend/src/routes/v1/webhook.ts b/backend/src/routes/v1/webhook.ts index 16264d4ed..30c59a15b 100644 --- a/backend/src/routes/v1/webhook.ts +++ b/backend/src/routes/v1/webhook.ts @@ -1,74 +1,46 @@ import express from "express"; const router = express.Router(); -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { body, param, query } from "express-validator"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { webhookController } from "../../controllers/v1"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body" - }), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("webhookUrl").exists().isString().isURL().trim(), - body("webhookSecretKey").isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, webhookController.createWebhook ); router.patch( "/:webhookId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("webhookId").exists().isString().trim(), - body("isDisabled").default(false).isBoolean(), - validateRequest, webhookController.updateWebhook ); router.post( "/:webhookId/test", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("webhookId").exists().isString().trim(), - validateRequest, webhookController.testWebhook ); router.delete( "/:webhookId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("webhookId").exists().isString().trim(), - validateRequest, webhookController.deleteWebhook ); router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query" - }), - query("workspaceId").exists().isString().trim(), - query("environment").optional().isString().trim(), - query("secretPath").optional().isString().trim(), - validateRequest, webhookController.listWebhooks ); diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index f08d92865..c8acc1846 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -1,163 +1,95 @@ import express from "express"; const router = express.Router(); import { body, param } from "express-validator"; -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; import { membershipController, workspaceController } from "../../controllers/v1"; router.get( - "/:workspaceId/keys", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspacePublicKeys + "/:workspaceId/keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspacePublicKeys ); router.get( - "/:workspaceId/users", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceMemberships + "/:workspaceId/users", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceMemberships ); router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - workspaceController.getWorkspaces + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.getWorkspaces ); router.get( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspace + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspace ); router.post( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("workspaceName").exists().trim().notEmpty(), - body("organizationId").exists().trim().notEmpty(), - validateRequest, - workspaceController.createWorkspace -); - -router.delete( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.deleteWorkspace + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.deleteWorkspace ); router.post( - "/:workspaceId/name", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("name").exists().trim().notEmpty(), - validateRequest, - workspaceController.changeWorkspaceName + "/:workspaceId/name", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params" + }), + param("workspaceId").exists().trim(), + body("name").exists().trim().notEmpty(), + validateRequest, + workspaceController.changeWorkspaceName ); router.post( - "/:workspaceId/invite-signup", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("email").exists().trim().notEmpty(), - validateRequest, - membershipController.inviteUserToWorkspace + "/:workspaceId/invite-signup", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.inviteUserToWorkspace ); router.get( - "/:workspaceId/integrations", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceIntegrations + "/:workspaceId/integrations", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceIntegrations ); router.get( - "/:workspaceId/authorizations", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceIntegrationAuthorizations + "/:workspaceId/authorizations", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceIntegrationAuthorizations ); router.get( - "/:workspaceId/service-tokens", // TODO endpoint: deprecate - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceServiceTokens + "/:workspaceId/service-tokens", // TODO endpoint: deprecate + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceServiceTokens ); export default router; diff --git a/backend/src/routes/v2/auth.ts b/backend/src/routes/v2/auth.ts index e2d45f452..c7aa6b066 100644 --- a/backend/src/routes/v2/auth.ts +++ b/backend/src/routes/v2/auth.ts @@ -5,7 +5,8 @@ import { requireMfaAuth, validateRequest } from "../../middleware"; import { authController } from "../../controllers/v2"; import { authLimiter } from "../../helpers/rateLimiter"; -router.post( // TODO: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, body("email").isString().trim().notEmpty().toLowerCase(), @@ -14,7 +15,8 @@ router.post( // TODO: deprecate (moved to api/v3/auth/login1) authController.login1 ); -router.post( // TODO: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO: deprecate (moved to api/v3/auth/login1) "/login2", authLimiter, body("email").isString().trim().notEmpty().toLowerCase(), @@ -23,22 +25,9 @@ router.post( // TODO: deprecate (moved to api/v3/auth/login1) authController.login2 ); -router.post( - "/mfa/send", - authLimiter, - body("email").isString().trim().notEmpty().isEmail(), - validateRequest, - authController.sendMfaToken -); +//remove above ones after depreciation +router.post("/mfa/send", authLimiter, authController.sendMfaToken); -router.post( - "/mfa/verify", - authLimiter, - requireMfaAuth, - body("email").isString().trim().notEmpty(), - body("mfaToken").isString().trim().notEmpty(), - validateRequest, - authController.verifyMfaToken -); +router.post("/mfa/verify", authLimiter, requireMfaAuth, authController.verifyMfaToken); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index a6d5baa13..b870f2813 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -1,48 +1,22 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; import { environmentController } from "../../controllers/v2"; -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; router.post( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().trim(), - body("environmentName").exists().trim(), - validateRequest, environmentController.createWorkspaceEnvironment ); router.put( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().trim(), - body("environmentName").exists().trim(), - body("oldEnvironmentSlug").exists().trim(), - validateRequest, environmentController.renameWorkspaceEnvironment ); @@ -67,30 +41,17 @@ router.patch( router.delete( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().trim(), - validateRequest, environmentController.deleteWorkspaceEnvironment ); router.get( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, environmentController.getAllAccessibleEnvironmentsOfWorkspace ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index cb60035a7..3a8395bc6 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -1,14 +1,12 @@ import express from "express"; const router = express.Router(); -import { Types } from "mongoose"; import { requireAuth, requireSecretsAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { validateClientForSecrets } from "../../validation"; -import { body, query } from "express-validator"; +import { body } from "express-validator"; import { secretsController } from "../../controllers/v2"; import { ADMIN, @@ -19,9 +17,9 @@ import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; -import { BatchSecretRequest } from "../../types/secret"; -router.post( // TODO endpoint: strongly consider deprecation in favor of a single operation experience on dashboard +router.post( + // TODO endpoint: strongly consider deprecation in favor of a single operation experience on dashboard "/batch", requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] @@ -30,33 +28,11 @@ router.post( // TODO endpoint: strongly consider deprecation in favor of a singl acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body" }), - body("workspaceId").exists().isString().trim(), - body("folderId").default("root").isString().trim(), - body("environment").exists().isString().trim(), - body("secretPath").optional().isString().trim(), - body("requests") - .exists() - .custom(async (requests: BatchSecretRequest[], { req }) => { - if (Array.isArray(requests)) { - const secretIds = requests - .map((request) => request.secret._id) - .filter((secretId) => secretId !== undefined); - - if (secretIds.length > 0) { - req.secrets = await validateClientForSecrets({ - authData: req.authData, - secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), - requiredPermissions: [] - }); - } - } - return true; - }), - validateRequest, secretsController.batchSecrets ); -router.post( // TODO endpoint: deprecate (moved to POST api/v3/secrets) +router.post( + // TODO endpoint: deprecate (moved to POST api/v3/secrets) "/", body("workspaceId").exists().isString().trim(), body("environment").exists().isString().trim(), @@ -117,15 +93,9 @@ router.post( // TODO endpoint: deprecate (moved to POST api/v3/secrets) secretsController.createSecrets ); -router.get( // TODO endpoint: deprecate (moved to GET api/v3/secrets) +router.get( + // TODO endpoint: deprecate (moved to GET api/v3/secrets) "/", - query("workspaceId").exists().trim(), - query("environment").exists().trim(), - query("tagSlugs"), - query("folderId").default("root").isString().trim(), - query("secretPath").optional().isString().trim(), - query("include_imports").optional().default(false).isBoolean(), - validateRequest, requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), @@ -138,7 +108,8 @@ router.get( // TODO endpoint: deprecate (moved to GET api/v3/secrets) secretsController.getSecrets ); -router.patch( // TODO endpoint: deprecate (moved to PATCH api/v3/secrets) +router.patch( + // TODO endpoint: deprecate (moved to PATCH api/v3/secrets) "/", body("secrets") .exists() @@ -173,7 +144,8 @@ router.patch( // TODO endpoint: deprecate (moved to PATCH api/v3/secrets) secretsController.updateSecrets ); -router.delete( // TODO endpoint: deprecate (moved to DELETE api/v3/secrets) +router.delete( + // TODO endpoint: deprecate (moved to DELETE api/v3/secrets) "/", body("secretIds") .exists() diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index efdc905fe..611cf6003 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -1,18 +1,9 @@ import express from "express"; const router = express.Router(); import { - requireAuth, - requireServiceTokenDataAuth, - requireWorkspaceAuth, - validateRequest + requireAuth } from "../../middleware"; -import { body, param } from "express-validator"; -import { - ADMIN, - AuthMode, - MEMBER, - PERMISSION_WRITE_SECRETS -} from "../../variables"; +import { AuthMode } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; router.get( @@ -28,33 +19,6 @@ router.post( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }), - body("name").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("scopes").exists().isArray(), - body("scopes.*.environment").exists().isString().trim(), - body("scopes.*.secretPath").exists().isString().trim(), - body("encryptedKey").exists().isString().trim(), - body("iv").exists().isString().trim(), - body("tag").exists().isString().trim(), - body("expiresIn").exists().isNumeric(), // measured in ms - body("permissions") - .isArray({ min: 1 }) - .custom((value: string[]) => { - const allowedPermissions = ["read", "write"]; - const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); - if (invalidValues.length > 0) { - throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`); - } - - return true; - }), - validateRequest, serviceTokenDataController.createServiceTokenData ); @@ -63,11 +27,6 @@ router.delete( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireServiceTokenDataAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param("serviceTokenDataId").exists().trim(), - validateRequest, serviceTokenDataController.deleteServiceTokenData ); diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index aca1b6d8c..7926e9452 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -1,56 +1,30 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; import { tagController } from "../../controllers/v2"; -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; router.get( "/:workspaceId/tags", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, tagController.getWorkspaceTags ); router.delete( "/tags/:tagId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("tagId").exists().trim(), - validateRequest, tagController.deleteWorkspaceTag ); router.post( "/:workspaceId/tags", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("name").exists().trim(), - body("tagColor").exists().trim(), - body("slug").exists().trim(), - validateRequest, tagController.createWorkspaceTag ); diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index c13abfcb9..c093be21c 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -1,114 +1,87 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - validateRequest, -} from "../../middleware"; -import { body, param } from "express-validator"; +import { requireAuth } from "../../middleware"; import { usersController } from "../../controllers/v2"; import { AuthMode } from "../../variables"; -import { - AuthMethod -} from "../../models"; router.get( - "/me", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - usersController.getMe + "/me", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.getMe ); router.patch( - "/me/mfa", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - body("isMfaEnabled").exists().isBoolean(), - validateRequest, - usersController.updateMyMfaEnabled + "/me/mfa", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.updateMyMfaEnabled ); router.patch( - "/me/name", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - body("firstName").exists().isString(), - body("lastName").isString(), - validateRequest, - usersController.updateName + "/me/name", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.updateName ); router.put( - "/me/auth-methods", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - body("authMethods").exists().isArray({ - min: 1, - }).custom((authMethods: AuthMethod[]) => { - return authMethods.every(provider => [ - AuthMethod.EMAIL, - AuthMethod.GOOGLE, - AuthMethod.GITHUB - ].includes(provider)) - }), - validateRequest, - usersController.updateAuthMethods, + "/me/auth-methods", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.updateAuthMethods ); router.get( - "/me/organizations", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - usersController.getMyOrganizations + "/me/organizations", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.getMyOrganizations ); router.get( - "/me/api-keys", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - usersController.getMyAPIKeys + "/me/api-keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.getMyAPIKeys ); router.post( - "/me/api-keys", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - body("name").exists().isString().trim(), - body("expiresIn").isNumeric(), - validateRequest, - usersController.createAPIKey + "/me/api-keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.createAPIKey ); router.delete( - "/me/api-keys/:apiKeyDataId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - param("apiKeyDataId").exists().trim(), - validateRequest, - usersController.deleteAPIKey + "/me/api-keys/:apiKeyDataId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.deleteAPIKey ); router.get( - "/me/sessions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - usersController.getMySessions + "/me/sessions", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.getMySessions ); router.delete( - "/me/sessions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - usersController.deleteMySessions + "/me/sessions", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.deleteMySessions ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index fde41d945..304f69b34 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -1,147 +1,96 @@ import express from "express"; const router = express.Router(); import { body, param, query } from "express-validator"; -import { - requireAuth, - requireMembershipAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; import { workspaceController } from "../../controllers/v2"; -router.post( // TODO endpoint: deprecate (moved to POST v3/secrets) - "/:workspaceId/secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - body("secrets").exists(), - body("keys").exists(), - body("environment").exists().trim().notEmpty(), - body("channel"), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.pushWorkspaceSecrets -); - -router.get( // TODO endpoint: deprecate (moved to GET v3/secrets) - "/:workspaceId/secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - query("environment").exists().trim(), - query("channel"), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.pullSecrets -); - -router.get( // TODO endpoint: consider moving to v3/users/me/workspaces/:workspaceId/key - "/:workspaceId/encrypted-key", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceKey +router.post( + // TODO endpoint: deprecate (moved to POST v3/secrets) + "/:workspaceId/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params" + }), + body("secrets").exists(), + body("keys").exists(), + body("environment").exists().trim().notEmpty(), + body("channel"), + param("workspaceId").exists().trim(), + validateRequest, + workspaceController.pushWorkspaceSecrets ); router.get( - "/:workspaceId/service-token-data", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceServiceTokenData + // TODO endpoint: deprecate (moved to GET v3/secrets) + "/:workspaceId/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params" + }), + query("environment").exists().trim(), + query("channel"), + param("workspaceId").exists().trim(), + validateRequest, + workspaceController.pullSecrets ); -router.get( // new - TODO: rewire dashboard to this route - "/:workspaceId/memberships", - param("workspaceId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - workspaceController.getWorkspaceMemberships +router.get( + // TODO endpoint: consider moving to v3/users/me/workspaces/:workspaceId/key + "/:workspaceId/encrypted-key", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.getWorkspaceKey ); -router.patch( // TODO - rewire dashboard to this route - "/:workspaceId/memberships/:membershipId", - param("workspaceId").exists().trim(), - param("membershipId").exists().trim(), - body("role").exists().isString().trim().isIn([ADMIN, MEMBER]), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - requireMembershipAuth({ - acceptedRoles: [ADMIN], - locationMembershipId: "params", - }), - workspaceController.updateWorkspaceMembership +router.get( + "/:workspaceId/service-token-data", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceServiceTokenData ); -router.delete( // TODO - rewire dashboard to this route - "/:workspaceId/memberships/:membershipId", - param("workspaceId").exists().trim(), - param("membershipId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - requireMembershipAuth({ - acceptedRoles: [ADMIN], - locationMembershipId: "params", - }), - workspaceController.deleteWorkspaceMembership +router.get( + // new - TODO: rewire dashboard to this route + "/:workspaceId/memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.getWorkspaceMemberships ); router.patch( - "/:workspaceId/auto-capitalization", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("autoCapitalization").exists().trim().notEmpty(), - validateRequest, - workspaceController.toggleAutoCapitalization + // TODO - rewire dashboard to this route + "/:workspaceId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.updateWorkspaceMembership +); + +router.delete( + // TODO - rewire dashboard to this route + "/:workspaceId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.deleteWorkspaceMembership +); + +router.patch( + "/:workspaceId/auto-capitalization", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.toggleAutoCapitalization ); export default router; diff --git a/backend/src/routes/v3/auth.ts b/backend/src/routes/v3/auth.ts index ba6f30335..44fdaef4b 100644 --- a/backend/src/routes/v3/auth.ts +++ b/backend/src/routes/v3/auth.ts @@ -1,29 +1,11 @@ import express from "express"; -import { body } from "express-validator"; -import { validateRequest } from "../../middleware"; import { authController } from "../../controllers/v3"; import { authLimiter } from "../../helpers/rateLimiter"; const router = express.Router(); -router.post( - "/login1", - authLimiter, - body("email").isString().trim().toLowerCase(), - body("providerAuthToken").isString().trim().optional({nullable: true}), - body("clientPublicKey").isString().trim().notEmpty(), - validateRequest, - authController.login1 -); +router.post("/login1", authLimiter, authController.login1); -router.post( - "/login2", - authLimiter, - body("email").isString().trim().toLowerCase(), - body("providerAuthToken").isString().trim().optional({nullable: true}), - body("clientProof").isString().trim().notEmpty(), - validateRequest, - authController.login2 -); +router.post("/login2", authLimiter, authController.login2); export default router; diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 648fc57f5..5d6b8f772 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -15,36 +15,18 @@ import { router.get( "/raw", - query("workspaceId").optional().isString().trim(), - query("environment").optional().isString().trim(), - query("folderId").optional().isString().trim(), - query("secretPath").default("/").isString().trim(), - query("include_imports").optional().isBoolean().default(false), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), secretsController.getSecretsRaw ); +// TODO(akhilmhdh): tony please split the requireWorkspaceAuth to multiple middlewares +// IP checking into another one router.get( "/raw/:secretName", - param("secretName").exists().isString().trim(), - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("secretPath").default("/").isString().trim(), - query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -60,19 +42,8 @@ router.get( router.post( "/raw/:secretName", - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretValue").exists().isString().trim(), - body("secretComment").default("").isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -88,19 +59,8 @@ router.post( router.patch( "/raw/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretValue").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -116,18 +76,8 @@ router.patch( router.delete( "/raw/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -149,11 +99,7 @@ router.get( query("secretPath").default("/").isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -169,28 +115,8 @@ router.get( router.post( "/:secretName", - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretKeyCiphertext").exists().isString().trim(), - body("secretKeyIV").exists().isString().trim(), - body("secretKeyTag").exists().isString().trim(), - body("secretValueCiphertext").exists().isString().trim(), - body("secretValueIV").exists().isString().trim(), - body("secretValueTag").exists().isString().trim(), - body("secretCommentCiphertext").optional().isString().trim(), - body("secretCommentIV").optional().isString().trim(), - body("secretCommentTag").optional().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("metadata").optional().isObject().withMessage("Metadata should be an object"), - body("metadata.source").optional().isString().withMessage("Source should be a string"), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -206,18 +132,8 @@ router.post( router.get( "/:secretName", - param("secretName").exists().isString().trim(), - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("secretPath").default("/").isString().trim(), - query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -232,21 +148,8 @@ router.get( router.patch( "/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretValueCiphertext").exists().isString().trim(), - body("secretValueIV").exists().isString().trim(), - body("secretValueTag").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -269,11 +172,7 @@ router.delete( body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v3/signup.ts b/backend/src/routes/v3/signup.ts index bfd0c9c4d..241f5d04c 100644 --- a/backend/src/routes/v3/signup.ts +++ b/backend/src/routes/v3/signup.ts @@ -1,30 +1,14 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; import { signupController } from "../../controllers/v3"; import { authLimiter } from "../../helpers/rateLimiter"; import { validateRequest } from "../../middleware"; router.post( - "/complete-account/signup", // TODO: consider moving endpoint to v3/users/new/complete-account/signup - authLimiter, - body("email").exists().isString().trim().notEmpty().isEmail(), - body("firstName").exists().isString().trim().notEmpty(), - body("lastName").exists().isString().trim().optional({nullable: true}), - body("protectedKey").exists().isString().trim().notEmpty(), - body("protectedKeyIV").exists().isString().trim().notEmpty(), - body("protectedKeyTag").exists().isString().trim().notEmpty(), - body("publicKey").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), - body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), - body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), - body("salt").exists().isString().trim().notEmpty(), - body("verifier").exists().isString().trim().notEmpty(), - body("organizationName").exists().isString().trim().notEmpty(), - body("providerAuthToken").isString().trim().optional({ nullable: true }), - body("attributionSource").optional().isString().trim(), - validateRequest, - signupController.completeAccountSignup, + "/complete-account/signup", // TODO: consider moving endpoint to v3/users/new/complete-account/signup + authLimiter, + validateRequest, + signupController.completeAccountSignup ); export default router; diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts index 38695e8b9..834d54cd1 100644 --- a/backend/src/routes/v3/workspaces.ts +++ b/backend/src/routes/v3/workspaces.ts @@ -1,79 +1,37 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; +import { requireAuth } from "../../middleware"; import { workspacesController } from "../../controllers/v3"; -import { - ADMIN, - AuthMode -} from "../../variables"; -import { body, param } from "express-validator"; +import { AuthMode } from "../../variables"; // -- migration to blind indices endpoints router.get( - "/:workspaceId/secrets/blind-index-status", - param("workspaceId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - workspacesController.getWorkspaceBlindIndexStatus + "/:workspaceId/secrets/blind-index-status", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.getWorkspaceBlindIndexStatus ); -router.get( // allow admins to get all workspace secrets (part of blind indices migration) - "/:workspaceId/secrets", - param("workspaceId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - workspacesController.getWorkspaceSecrets +router.get( + // allow admins to get all workspace secrets (part of blind indices migration) + "/:workspaceId/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.getWorkspaceSecrets ); -router.post( // allow admins to name all workspace secrets (part of blind indices migration) - "/:workspaceId/secrets/names", - param("workspaceId").exists().isString().trim(), - body("secretsToUpdate") - .exists() - .isArray() - .withMessage("secretsToUpdate must be an array") - .customSanitizer((value) => { - return value.map((secret: any) => ({ - secretName: secret.secretName, - _id: secret._id, - })); - }), - body("secretsToUpdate.*.secretName") - .exists() - .isString() - .withMessage("secretName must be a string"), - body("secretsToUpdate.*._id") - .exists() - .isString() - .withMessage("secretId must be a string"), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - workspacesController.nameWorkspaceSecrets +router.post( + // allow admins to name all workspace secrets (part of blind indices migration) + "/:workspaceId/secrets/names", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.nameWorkspaceSecrets ); // -- -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/services/ProjectRoleService.ts b/backend/src/services/ProjectRoleService.ts index 02f992aa1..6a42d6de3 100644 --- a/backend/src/services/ProjectRoleService.ts +++ b/backend/src/services/ProjectRoleService.ts @@ -1,16 +1,22 @@ -import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; +import { + AbilityBuilder, + ForcedSubject, + MongoAbility, + RawRuleOf, + createMongoAbility +} from "@casl/ability"; import { Membership } from "../models"; import { IRole } from "../models/role"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; -export enum GeneralPermissionActions { +export enum ProjectPermissionActions { Read = "read", Create = "create", Edit = "edit", Delete = "delete" } -export enum ProjectPermission { +export enum ProjectPermissionSub { Role = "role", Member = "member", Settings = "settings", @@ -24,96 +30,119 @@ export enum ProjectPermission { Workspace = "workspace", Secrets = "secrets", SecretImports = "secret-imports", + SecretRollback = "secret-rollback", Folders = "folders" } +type GenericFields = { + environment: string; + secretPath?: string; +}; + export type ProjectPermissionSet = - | [GeneralPermissionActions, ProjectPermission.Secrets] - | [GeneralPermissionActions, ProjectPermission.Folders] - | [GeneralPermissionActions, ProjectPermission.SecretImports] - | [GeneralPermissionActions, ProjectPermission.Role] - | [GeneralPermissionActions, ProjectPermission.Tags] - | [GeneralPermissionActions, ProjectPermission.Member] - | [GeneralPermissionActions, ProjectPermission.Integrations] - | [GeneralPermissionActions, ProjectPermission.Webhooks] - | [GeneralPermissionActions, ProjectPermission.AuditLogs] - | [GeneralPermissionActions, ProjectPermission.Environments] - | [GeneralPermissionActions, ProjectPermission.IpAllowList] - | [GeneralPermissionActions, ProjectPermission.Settings] - | [GeneralPermissionActions, ProjectPermission.ServiceTokens] - | [GeneralPermissionActions.Delete, ProjectPermission.Workspace] - | [GeneralPermissionActions.Edit, ProjectPermission.Workspace]; + | [ + ProjectPermissionActions, + ProjectPermissionSub.Secrets | (ForcedSubject & GenericFields) + ] + | [ + ProjectPermissionActions, + ProjectPermissionSub.Folders | (ForcedSubject & GenericFields) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & GenericFields) + ) + ] + | [ProjectPermissionActions, ProjectPermissionSub.Role] + | [ProjectPermissionActions, ProjectPermissionSub.Tags] + | [ProjectPermissionActions, ProjectPermissionSub.Member] + | [ProjectPermissionActions, ProjectPermissionSub.Integrations] + | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] + | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] + | [ProjectPermissionActions, ProjectPermissionSub.Environments] + | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] + | [ProjectPermissionActions, ProjectPermissionSub.Settings] + | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] + | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; const buildAdminPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); - can(GeneralPermissionActions.Read, ProjectPermission.Secrets); - can(GeneralPermissionActions.Create, ProjectPermission.Secrets); - can(GeneralPermissionActions.Edit, ProjectPermission.Secrets); - can(GeneralPermissionActions.Delete, ProjectPermission.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); - can(GeneralPermissionActions.Read, ProjectPermission.Folders); - can(GeneralPermissionActions.Create, ProjectPermission.Folders); - can(GeneralPermissionActions.Edit, ProjectPermission.Folders); - can(GeneralPermissionActions.Delete, ProjectPermission.Folders); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Folders); - can(GeneralPermissionActions.Read, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Create, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Edit, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Delete, ProjectPermission.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretImports); - can(GeneralPermissionActions.Read, ProjectPermission.Member); - can(GeneralPermissionActions.Create, ProjectPermission.Member); - can(GeneralPermissionActions.Edit, ProjectPermission.Member); - can(GeneralPermissionActions.Delete, ProjectPermission.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); - can(GeneralPermissionActions.Read, ProjectPermission.Role); - can(GeneralPermissionActions.Create, ProjectPermission.Role); - can(GeneralPermissionActions.Edit, ProjectPermission.Role); - can(GeneralPermissionActions.Delete, ProjectPermission.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); - can(GeneralPermissionActions.Read, ProjectPermission.Integrations); - can(GeneralPermissionActions.Create, ProjectPermission.Integrations); - can(GeneralPermissionActions.Edit, ProjectPermission.Integrations); - can(GeneralPermissionActions.Delete, ProjectPermission.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); - can(GeneralPermissionActions.Read, ProjectPermission.Webhooks); - can(GeneralPermissionActions.Create, ProjectPermission.Webhooks); - can(GeneralPermissionActions.Edit, ProjectPermission.Webhooks); - can(GeneralPermissionActions.Delete, ProjectPermission.Webhooks); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); - can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens); - can(GeneralPermissionActions.Create, ProjectPermission.ServiceTokens); - can(GeneralPermissionActions.Edit, ProjectPermission.ServiceTokens); - can(GeneralPermissionActions.Delete, ProjectPermission.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); - can(GeneralPermissionActions.Read, ProjectPermission.Settings); - can(GeneralPermissionActions.Create, ProjectPermission.Settings); - can(GeneralPermissionActions.Edit, ProjectPermission.Settings); - can(GeneralPermissionActions.Delete, ProjectPermission.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); - can(GeneralPermissionActions.Read, ProjectPermission.Environments); - can(GeneralPermissionActions.Create, ProjectPermission.Environments); - can(GeneralPermissionActions.Edit, ProjectPermission.Environments); - can(GeneralPermissionActions.Delete, ProjectPermission.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); - can(GeneralPermissionActions.Read, ProjectPermission.Tags); - can(GeneralPermissionActions.Create, ProjectPermission.Tags); - can(GeneralPermissionActions.Edit, ProjectPermission.Tags); - can(GeneralPermissionActions.Delete, ProjectPermission.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); - can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs); - can(GeneralPermissionActions.Create, ProjectPermission.AuditLogs); - can(GeneralPermissionActions.Edit, ProjectPermission.AuditLogs); - can(GeneralPermissionActions.Delete, ProjectPermission.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); - can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList); - can(GeneralPermissionActions.Create, ProjectPermission.IpAllowList); - can(GeneralPermissionActions.Edit, ProjectPermission.IpAllowList); - can(GeneralPermissionActions.Delete, ProjectPermission.IpAllowList); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs); - can(GeneralPermissionActions.Edit, ProjectPermission.Workspace); - can(GeneralPermissionActions.Delete, ProjectPermission.IpAllowList); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList); + + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList); return build(); }; @@ -123,31 +152,33 @@ export const adminProjectPermissions = buildAdminPermission(); const buildMemberPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); - can(GeneralPermissionActions.Read, ProjectPermission.Secrets); - can(GeneralPermissionActions.Create, ProjectPermission.Secrets); - can(GeneralPermissionActions.Edit, ProjectPermission.Secrets); - can(GeneralPermissionActions.Delete, ProjectPermission.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); - can(GeneralPermissionActions.Read, ProjectPermission.Folders); - can(GeneralPermissionActions.Create, ProjectPermission.Folders); - can(GeneralPermissionActions.Edit, ProjectPermission.Folders); - can(GeneralPermissionActions.Delete, ProjectPermission.Folders); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Folders); - can(GeneralPermissionActions.Read, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Create, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Edit, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Delete, ProjectPermission.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); - can(GeneralPermissionActions.Read, ProjectPermission.Member); - can(GeneralPermissionActions.Read, ProjectPermission.Role); - can(GeneralPermissionActions.Read, ProjectPermission.Integrations); - can(GeneralPermissionActions.Read, ProjectPermission.Webhooks); - can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens); - can(GeneralPermissionActions.Read, ProjectPermission.Settings); - can(GeneralPermissionActions.Read, ProjectPermission.Environments); - can(GeneralPermissionActions.Read, ProjectPermission.Tags); - can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs); - can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); return build(); }; @@ -157,19 +188,20 @@ export const memberProjectPermissions = buildMemberPermission(); const buildViewerPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); - can(GeneralPermissionActions.Read, ProjectPermission.Secrets); - can(GeneralPermissionActions.Read, ProjectPermission.Folders); - can(GeneralPermissionActions.Read, ProjectPermission.SecretImports); - can(GeneralPermissionActions.Read, ProjectPermission.Member); - can(GeneralPermissionActions.Read, ProjectPermission.Role); - can(GeneralPermissionActions.Read, ProjectPermission.Integrations); - can(GeneralPermissionActions.Read, ProjectPermission.Webhooks); - can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens); - can(GeneralPermissionActions.Read, ProjectPermission.Settings); - can(GeneralPermissionActions.Read, ProjectPermission.Environments); - can(GeneralPermissionActions.Read, ProjectPermission.Tags); - can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs); - can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); return build(); }; diff --git a/backend/src/validation/action.ts b/backend/src/validation/action.ts new file mode 100644 index 000000000..7c76a5365 --- /dev/null +++ b/backend/src/validation/action.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const GetActionV1 = z.object({ + params: z.object({ + actionId: z.string().trim() + }) +}); + +export const AddUserActionV1 = z.object({ + body: z.object({ + action: z.string().trim() + }) +}); + +export const GetUserActionV1 = z.object({ + query: z.object({ + action: z.string().trim() + }) +}); diff --git a/backend/src/validation/auth.ts b/backend/src/validation/auth.ts new file mode 100644 index 000000000..3cf292477 --- /dev/null +++ b/backend/src/validation/auth.ts @@ -0,0 +1,134 @@ +import { z } from "zod"; + +export const BeginEmailSignUpV1 = z.object({ + body: z.object({ + email: z.string().email().trim() + }) +}); + +export const VerifyEmailSignUpV1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + code: z.string().email().trim() + }) +}); + +export const Login1V1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + clientPublicKey: z.string().trim() + }) +}); + +export const Login2V1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + clientProof: z.string().trim() + }) +}); + +export const Srp1V1 = z.object({ + body: z.object({ + clientPublicKey: z.string().trim() + }) +}); + +export const ChangePasswordV1 = z.object({ + body: z.object({ + clientProof: z.string().trim(), + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }) +}); + +export const EmailPasswordResetV1 = z.object({ + body: z.object({ + email: z.string().email().trim() + }) +}); + +export const EmailPasswordResetVerifyV1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + code: z.string().trim() + }) +}); + +export const CreateBackupPrivateKeyV1 = z.object({ + body: z.object({ + clientProof: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + iv: z.string().trim(), + tag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }) +}); + +export const ResetPasswordV1 = z.object({ + body: z.object({ + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }) +}); + +export const SendMfaTokenV2 = z.object({ + body: z.object({ + email: z.string().email().trim() + }) +}); + +export const VerifyMfaTokenV2 = z.object({ + body: z.object({ + email: z.string().email().trim(), + mfaToken: z.string().trim() + }) +}); + +export const Login1V3 = z.object({ + body: z.object({ + email: z.string().email().trim(), + providerAuthToken: z.string().trim().optional(), + clientPublicKey: z.string().trim() + }) +}); + +export const Login2V3 = z.object({ + body: z.object({ + email: z.string().email().trim(), + providerAuthToken: z.string().trim().optional(), + clientProof: z.string().trim() + }) +}); + +export const CompletedAccountSignupV3 = z.object({ + body: z.object({ + email: z.string().email().trim(), + firstName: z.string().trim(), + lastName: z.string().trim().optional().nullish(), + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + publicKey: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim(), + organizationName: z.string().trim(), + providerAuthToken: z.string().trim().optional().nullish(), + attributionSource: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/bot.ts b/backend/src/validation/bot.ts index 2bb6ec6dd..2980aeac1 100644 --- a/backend/src/validation/bot.ts +++ b/backend/src/validation/bot.ts @@ -1,15 +1,10 @@ import { Types } from "mongoose"; -import { - Bot, - IUser, -} from "../models"; +import { Bot, IUser } from "../models"; import { validateUserClientForWorkspace } from "./user"; -import { - BotNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for bot with id [botId] based @@ -22,7 +17,7 @@ import { ActorType } from "../ee/models"; export const validateClientForBot = async ({ authData, botId, - acceptedRoles, + acceptedRoles }: { authData: AuthData; botId: Types.ObjectId; @@ -30,18 +25,39 @@ export const validateClientForBot = async ({ }) => { const bot = await Bot.findById(botId); if (!bot) throw BotNotFoundError(); - + switch (authData.actor.type) { case ActorType.USER: await validateUserClientForWorkspace({ user: authData.authPayload as IUser, workspaceId: bot.workspace, - acceptedRoles, + acceptedRoles }); return bot; case ActorType.SERVICE: throw UnauthorizedRequestError({ - message: "Failed service token authorization for bot", + message: "Failed service token authorization for bot" }); } -}; \ No newline at end of file +}; + +export const GetBotByWorkspaceIdV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const SetBotActiveStateV1 = z.object({ + body: z.object({ + isActive: z.boolean(), + botKey: z + .object({ + nonce: z.string().trim().optional(), + encryptedKey: z.string().trim().optional() + }) + .optional() + }), + params: z.object({ + botId: z.string().trim() + }) +}); diff --git a/backend/src/validation/cloudProducts.ts b/backend/src/validation/cloudProducts.ts new file mode 100644 index 000000000..1cfd361b3 --- /dev/null +++ b/backend/src/validation/cloudProducts.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const GetCloudProductsV1 = z.object({ + query: z.object({ + "billing-cycle": z.enum(["monthly", "yearly"]) + }) +}); diff --git a/backend/src/validation/environments.ts b/backend/src/validation/environments.ts new file mode 100644 index 000000000..dcdab72be --- /dev/null +++ b/backend/src/validation/environments.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +export const CreateWorkspaceEnvironmentV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim(), + environmentName: z.string().trim() + }) +}); + +export const UpdateWorkspaceEnvironmentV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim(), + environmentName: z.string().trim(), + oldEnvironmentSlug: z.string().trim() + }) +}); + +export const DeleteWorkspaceEnvironmentV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim() + }) +}); + +export const GetAllAccessibileEnvironmentsOfWorkspaceV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); diff --git a/backend/src/validation/folders.ts b/backend/src/validation/folders.ts new file mode 100644 index 000000000..ecfadea1c --- /dev/null +++ b/backend/src/validation/folders.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const CreateFolderV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderName: z.string().trim(), + parentFolderId: z.string().trim().optional() + }) +}); + +export const UpdateFolderV1 = z.object({ + params: z.object({ + folderId: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + name: z.string().trim() + }) +}); + +export const DeleteFolderV1 = z.object({ + params: z.object({ + folderId: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim() + }) +}); + +export const GetFoldersV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + parentFolderId: z.string().trim().optional(), + parentFolderPath: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index b1143a9f7..781b8bba4 100644 --- a/backend/src/validation/integration.ts +++ b/backend/src/validation/integration.ts @@ -1,18 +1,15 @@ import { Types } from "mongoose"; -import { - IUser, - Integration, - IntegrationAuth, -} from "../models"; +import { IUser, Integration, IntegrationAuth } from "../models"; import { validateUserClientForWorkspace } from "./user"; import { IntegrationService } from "../services"; import { - IntegrationAuthNotFoundError, - IntegrationNotFoundError, - UnauthorizedRequestError, + IntegrationAuthNotFoundError, + IntegrationNotFoundError, + UnauthorizedRequestError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for integration with id [integrationId] based @@ -25,42 +22,80 @@ import { ActorType } from "../ee/models"; * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForIntegration = async ({ - authData, - integrationId, - acceptedRoles, + authData, + integrationId, + acceptedRoles }: { - authData: AuthData; - integrationId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; + authData: AuthData; + integrationId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; }) => { - - const integration = await Integration.findById(integrationId); - if (!integration) throw IntegrationNotFoundError(); + const integration = await Integration.findById(integrationId); + if (!integration) throw IntegrationNotFoundError(); - const integrationAuth = await IntegrationAuth - .findById(integration.integrationAuth) - .select( - "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" - ); - - if (!integrationAuth) throw IntegrationAuthNotFoundError(); + const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth).select( + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + ); - const accessToken = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id, - })).accessToken; - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId: integration.workspace, - acceptedRoles, - }); - - return ({ integration, accessToken }); - case ActorType.SERVICE: - throw UnauthorizedRequestError({ - message: "Failed service token authorization for integration", - }); - } -} \ No newline at end of file + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + const accessToken = ( + await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + }) + ).accessToken; + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: integration.workspace, + acceptedRoles + }); + + return { integration, accessToken }; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration" + }); + } +}; + +export const CreateIntegrationV1 = z.object({ + body: z.object({ + integrationAuthId: z.string().trim(), + app: z.string().trim(), + isActive: z.boolean(), + appId: z.string().trim(), + secretPath: z.string().trim().default("/"), + sourceEnvironment: z.string().trim(), + targetEnvironment: z.string().trim(), + targetEnvironmentId: z.string().trim(), + targetService: z.string().trim(), + targetServiceId: z.string().trim(), + owner: z.string().trim(), + path: z.string().trim(), + region: z.string().trim() + }) +}); + +export const UpdateIntegrationV1 = z.object({ + params: z.object({ + integrationId: z.string().trim() + }), + body: z.object({ + app: z.string().trim(), + appId: z.string().trim(), + isActive: z.boolean(), + secretPath: z.string().trim().default("/"), + targetEnvironment: z.string().trim(), + owner: z.string().trim(), + environment: z.string().trim() + }) +}); + +export const DeleteIntegrationV1 = z.object({ + params: z.object({ + integrationId: z.string().trim() + }) +}); diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 676184324..dbefd7cec 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -1,17 +1,11 @@ import { Types } from "mongoose"; -import { - IUser, - IWorkspace, - IntegrationAuth, -} from "../models"; -import { - IntegrationAuthNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { IUser, IWorkspace, IntegrationAuth } from "../models"; +import { IntegrationAuthNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { IntegrationService } from "../services"; import { validateUserClientForWorkspace } from "./user"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for integration authorization with id [integrationAuthId] based @@ -22,53 +16,147 @@ import { ActorType } from "../ee/models"; * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ - const validateClientForIntegrationAuth = async ({ - authData, - integrationAuthId, - acceptedRoles, - attachAccessToken, +const validateClientForIntegrationAuth = async ({ + authData, + integrationAuthId, + acceptedRoles, + attachAccessToken }: { - authData: AuthData; - integrationAuthId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; - attachAccessToken?: boolean; + authData: AuthData; + integrationAuthId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; + attachAccessToken?: boolean; }) => { + const integrationAuth = await IntegrationAuth.findById(integrationAuthId) + .populate<{ workspace: IWorkspace }>("workspace") + .select( + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + ); - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .populate<{ workspace: IWorkspace }>("workspace") - .select( - "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" - ); - - if (!integrationAuth) throw IntegrationAuthNotFoundError(); - - let accessToken, accessId; - if (attachAccessToken) { - const access = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id, - })); - - accessToken = access.accessToken; - accessId = access.accessId; - } - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId: integrationAuth.workspace._id, - acceptedRoles, - }); + if (!integrationAuth) throw IntegrationAuthNotFoundError(); - return ({ integrationAuth, accessToken, accessId }); - case ActorType.SERVICE: - throw UnauthorizedRequestError({ - message: "Failed service token authorization for integration authorization", - }); - } -} + let accessToken, accessId; + if (attachAccessToken) { + const access = await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + }); -export { - validateClientForIntegrationAuth, -}; \ No newline at end of file + accessToken = access.accessToken; + accessId = access.accessId; + } + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: integrationAuth.workspace._id, + acceptedRoles + }); + + return { integrationAuth, accessToken, accessId }; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration authorization" + }); + } +}; + +export const GetIntegrationAuthV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const OauthExchangeV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + code: z.string().trim(), + integration: z.string().trim(), + url: z.string().trim().url().optional(), + }) +}); + +export const SaveIntegrationAccessTokenV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + accessId: z.string().trim().optional(), + accessToken: z.string().trim().optional(), + url: z.string().url().trim(), + namespace: z.string().trim(), + integration: z.string().trim(), + refreshToken:z.string().trim().optional() + }) +}); + +export const GetIntegrationAuthAppsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + teamId: z.string().trim(), + workspaceSlug: z.string().trim() + }) +}); + +export const GetIntegrationAuthTeamsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const GetIntegrationAuthVercelBranchesV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const GetIntegrationAuthRailwayEnvironmentsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const GetIntegrationAuthRailwayServicesV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const GetIntegrationAuthBitbucketWorkspacesV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const GetIntegrationAuthNorthflankSecretGroupsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const DeleteIntegrationAuthV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const GetIntegrationAuthTeamCityBuildConfigsV1 = z.object({ + params:z.object({ + appId:z.string().trim(), + integrationAuthId:z.string().trim() + }) +}) + +export { validateClientForIntegrationAuth }; diff --git a/backend/src/validation/key.ts b/backend/src/validation/key.ts new file mode 100644 index 000000000..a2d4ab92d --- /dev/null +++ b/backend/src/validation/key.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const UploadKeyV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + key: z.object({ + encryptedKey: z.string().trim(), + nonce: z.string().trim(), + userId: z.string().trim() + }) + }) +}); + +export const GetLatestKeyV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index 6c40aa0aa..c46cb2016 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -1,16 +1,12 @@ import { Types } from "mongoose"; -import { - IServiceTokenData, - IUser, - Membership, -} from "../models"; +import { IServiceTokenData, IUser, Membership } from "../models"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; -import { - MembershipNotFoundError, -} from "../utils/errors"; +import { MembershipNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; +import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; /** * Validate authenticated clients for membership with id [membershipId] based @@ -22,36 +18,64 @@ import { ActorType } from "../ee/models"; * @returns {Membership} - validated membership */ export const validateClientForMembership = async ({ - authData, - membershipId, - acceptedRoles, + authData, + membershipId, + acceptedRoles }: { - authData: AuthData; - membershipId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; + authData: AuthData; + membershipId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; }) => { - - const membership = await Membership.findById(membershipId); - - if (!membership) throw MembershipNotFoundError({ - message: "Failed to find membership", - }); - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId: membership.workspace, - acceptedRoles, - }); - - return membership; - case ActorType.SERVICE: - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(membership.workspace), - }); - - return membership; - } -} \ No newline at end of file + const membership = await Membership.findById(membershipId); + + if (!membership) + throw MembershipNotFoundError({ + message: "Failed to find membership" + }); + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: membership.workspace, + acceptedRoles + }); + + return membership; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(membership.workspace) + }); + + return membership; + } +}; + +export const ValidateMembershipV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const DeleteMembershipV1 = z.object({ + params: z.object({ + membershipId: z.string().trim() + }) +}); + +export const ChangeMembershipRoleV1 = z.object({ + body: z.object({ + role: z.enum([ADMIN, VIEWER, MEMBER, CUSTOM]) + }), + params: z.object({ membershipId: z.string().trim() }) +}); + +export const DenyMembershipPermissionV1 = z.object({ + params: z.object({ + membershipId: z.string().trim() + }), + body: z.object({ + permissions: z.object({}).array() + }) +}); diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index ba4fef5b4..9aa13cbb6 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -197,3 +197,11 @@ export const DeleteOrgMemberv2 = z.object({ export const GetOrgWorkspacesv2 = z.object({ params: z.object({ organizationId: z.string().trim() }) }); + +export const VerfiyUserToOrganizationV1 = z.object({ + body: z.object({ + email: z.string().trim().email(), + organizationId: z.string().trim(), + code: z.string().trim() + }) +}); diff --git a/backend/src/validation/secretImports.ts b/backend/src/validation/secretImports.ts new file mode 100644 index 000000000..7c27667bc --- /dev/null +++ b/backend/src/validation/secretImports.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +export const CreateSecretImportV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderId: z.string().trim().default("root"), + secretImport: z.object({ + environment: z.string().trim(), + secretPath: z.string().trim() + }) + }) +}); + +export const UpdateSecretImportV1 = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + secretImports: z + .object({ + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + }) +}); + +export const DeleteSecretImportV1 = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + secretImportPath: z.string().trim(), + secretImportEnv: z.string().trim() + }) +}); + +export const GetSecretImportsV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderId: z.string().trim() + }) +}); + +export const GetAllSecretsFromImportV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderId: z.string().trim().default("root") + }) +}); diff --git a/backend/src/validation/secretSnapshot.ts b/backend/src/validation/secretSnapshot.ts new file mode 100644 index 000000000..b431547e8 --- /dev/null +++ b/backend/src/validation/secretSnapshot.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const GetSecretSnapshotV1 = z.object({ + params: z.object({ + secretSnapshotId: z.string().trim() + }) +}); diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 6c0ac3084..a908a0fba 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -1,18 +1,15 @@ import { Types } from "mongoose"; -import { - ISecret, - IServiceTokenData, - IUser, - Secret, -} from "../models"; +import { ISecret, IServiceTokenData, IUser, Secret } from "../models"; import { validateUserClientForSecret, validateUserClientForSecrets } from "./user"; -import { validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { - BadRequestError, - SecretNotFoundError, -} from "../utils/errors"; + validateServiceTokenDataClientForSecrets, + validateServiceTokenDataClientForWorkspace +} from "./serviceTokenData"; +import { BadRequestError, SecretNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; +import { SECRET_PERSONAL, SECRET_SHARED } from "../variables"; /** * Validate authenticated clients for secrets with id [secretId] based @@ -24,42 +21,43 @@ import { ActorType } from "../ee/models"; * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForSecret = async ({ - authData, - secretId, - acceptedRoles, - requiredPermissions, + authData, + secretId, + acceptedRoles, + requiredPermissions }: { - authData: AuthData; - secretId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; - requiredPermissions: string[]; + authData: AuthData; + secretId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; + requiredPermissions: string[]; }) => { - const secret = await Secret.findById(secretId); + const secret = await Secret.findById(secretId); - if (!secret) throw SecretNotFoundError({ - message: "Failed to find secret", + if (!secret) + throw SecretNotFoundError({ + message: "Failed to find secret" }); - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForSecret({ - user: authData.authPayload as IUser, - secret, - acceptedRoles, - requiredPermissions, - }); - return secret; - case ActorType.SERVICE: - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload as IServiceTokenData, - workspaceId: secret.workspace, - environment: secret.environment, - }); - - return secret; - } -} + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForSecret({ + user: authData.authPayload as IUser, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: secret.workspace, + environment: secret.environment + }); + + return secret; + } +}; /** * Validate authenticated clients for secrets with ids [secretIds] based @@ -72,43 +70,292 @@ export const validateClientForSecret = async ({ * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForSecrets = async ({ - authData, - secretIds, - requiredPermissions, + authData, + secretIds, + requiredPermissions }: { - authData: AuthData; - secretIds: Types.ObjectId[]; - requiredPermissions: string[]; + authData: AuthData; + secretIds: Types.ObjectId[]; + requiredPermissions: string[]; }) => { + let secrets: ISecret[] = []; - let secrets: ISecret[] = []; - - secrets = await Secret.find({ - _id: { - $in: secretIds, - }, - }); + secrets = await Secret.find({ + _id: { + $in: secretIds + } + }); - if (secrets.length != secretIds.length) { - throw BadRequestError({ message: "Failed to validate non-existent secrets" }) - } - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForSecrets({ - user: authData.authPayload as IUser, - secrets, - requiredPermissions, - }); - - return secrets; - case ActorType.SERVICE: - await validateServiceTokenDataClientForSecrets({ - serviceTokenData: authData.authPayload as IServiceTokenData, - secrets, - requiredPermissions, - }); - - return secrets; - } -} \ No newline at end of file + if (secrets.length != secretIds.length) { + throw BadRequestError({ message: "Failed to validate non-existent secrets" }); + } + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForSecrets({ + user: authData.authPayload as IUser, + secrets, + requiredPermissions + }); + + return secrets; + case ActorType.SERVICE: + await validateServiceTokenDataClientForSecrets({ + serviceTokenData: authData.authPayload as IServiceTokenData, + secrets, + requiredPermissions + }); + + return secrets; + } +}; + +export const GetSecretVersionsV1 = z.object({ + params: z.object({ + secretId: z.string().trim() + }), + query: z.object({ + offset: z.number(), + limit: z.number() + }) +}); + +export const RollbackSecretVersionV1 = z.object({ + params: z.object({ + secretId: z.string().trim() + }), + body: z.object({ + version: z.number() + }) +}); + +export const PushSecretsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + secrets: z.object({}).array(), + keys: z.object({}).array(), + environment: z.string().trim(), + channel: z.string().trim() + }) +}); + +export const PullSecretsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + channel: z.string().optional(), + environment: z.string().trim() + }) +}); + +export const PullSecretsServiceTokenV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + channel: z.string().optional(), + environment: z.string().trim() + }) +}); + +const batchUpdateRequestV2 = z.object({ + _id: z.string(), + folderId: z.string().trim().optional(), + type: z.enum(["shared", "personal"]), + secretName: z.string().trim(), + secretKeyCiphertext: z.string().trim(), + secretKeyIV: z.string().trim(), + secretKeyTag: z.string().trim(), + secretValueCiphertext: z.string().trim(), + secretValueIV: z.string().trim(), + secretValueTag: z.string().trim(), + secretCommentCiphertext: z.string().trim().optional(), + secretCommentIV: z.string().trim().optional(), + secretCommentTag: z.string().trim().optional(), + tags: z.object({ + _id: z.string().trim(), + name: z.string().trim(), + slug: z.string().trim() + }) +}); + +export const BatchSecretsV2 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + folderId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().optional(), + requests: z + .discriminatedUnion("method", [ + z.object({ + method: z.literal("POST"), + secret: batchUpdateRequestV2.omit({ _id: true }) + }), + z.object({ + method: z.literal("PATCH"), + secret: batchUpdateRequestV2 + }), + z.object({ + method: z.literal("DELETE"), + secret: z.object({ _id: z.string().trim(), secretName: z.string().trim() }) + }) + ]) + .array() + }) +}); + +export const GetSecretsV2 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + tagSlugs: z.string().trim().optional(), + folderId: z.string().trim().default("root"), + secretPath: z.string().trim().optional(), + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }) +}); + +export const GetSecretsRawV3 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + folderId:z.string().trim().optional(), + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }) +}); + +export const GetSecretByNameRawV3 = z.object({ + params: z.object({ + secretName: z.string().trim() + }), + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional() + }) +}); + +export const CreateSecretRawV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + secretValue: z.string().trim(), + secretComment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]) + }), + params: z.object({ + secretName: z.string().trim() + }) +}); + +export const UpdateSecretByNameRawV3 = z.object({ + params: z.object({ + secretName: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretValue: z.string().trim(), + secretName: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) + }) +}); + +export const DeleteSecretByNameRawV3 = z.object({ + params: z.object({ + secretName: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) + }) +}); + +export const GetSecretsV3 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + folderId: z.string().trim().optional(), + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }) +}); + +export const GetSecretByNameV3 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional() + }), + params: z.object({ + secretName: z.string().trim() + }) +}); + +export const CreateSecretV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), + secretPath: z.string().trim().default("/"), + secretName: z.string().trim(), + secretKeyCiphertext: z.string().trim(), + secretKeyIV: z.string().trim(), + secretKeyTag: z.string().trim(), + secretValueCiphertext: z.string().trim(), + secretValueIV: z.string().trim(), + secretValueTag: z.string().trim(), + secretCommentCiphertext: z.string().trim().optional(), + secretCommentIV: z.string().trim().optional(), + secretCommentTag: z.string().trim().optional(), + metadata: z.object({ + source: z.string() + }) + }) +}); + +export const UpdateSecretByNameV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), + secretPath: z.string().trim().default("/"), + secretValueCiphertext: z.string().trim(), + secretValueIV: z.string().trim(), + secretValueTag: z.string().trim() + }), + params: z.object({ + secretName: z.string() + }) +}); + +export const DeleteSecretByNameV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), + secretPath: z.string().trim().default("/") + }), + params: z.object({ + secretName: z.string() + }) +}); diff --git a/backend/src/validation/serviceTokenData.ts b/backend/src/validation/serviceTokenData.ts index fe8e3bd0b..e7702306f 100644 --- a/backend/src/validation/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -1,14 +1,10 @@ import { Types } from "mongoose"; -import { - ISecret, - IServiceTokenData, - IUser, - ServiceTokenData, -} from "../models"; +import { ISecret, IServiceTokenData, IUser, ServiceTokenData } from "../models"; import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { validateUserClientForWorkspace } from "./user"; import { ActorType } from "../ee/models"; import { AuthData } from "../interfaces/middleware"; +import { z } from "zod"; /** * Validate authenticated clients for service token with id [serviceTokenId] based @@ -31,10 +27,11 @@ export const validateClientForServiceTokenData = async ({ .select("+encryptedKey +iv +tag") .populate<{ user: IUser }>("user"); - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ - message: "Failed to find service token data" - }); - + if (!serviceTokenData) + throw ServiceTokenDataNotFoundError({ + message: "Failed to find service token data" + }); + switch (authData.actor.type) { case ActorType.USER: await validateUserClientForWorkspace({ @@ -140,3 +137,28 @@ export const validateServiceTokenDataClientForSecrets = async ({ }); }); }; + +export const CreateServiceTokenV2 = z.object({ + body: z.object({ + name: z.string().trim(), + workspaceId: z.string().trim(), + scopes: z + .object({ + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + .min(1), + encryptedKey: z.string().trim(), + iv: z.string().trim(), + tag: z.string().trim(), + expiresIn: z.number(), + permissions: z.enum(["read", "write"]).array() + }) +}); + +export const DeleteServiceTokenV2 = z.object({ + params: z.object({ + serviceTokenDataId: z.string().trim() + }) +}); diff --git a/backend/src/validation/tags.ts b/backend/src/validation/tags.ts new file mode 100644 index 000000000..0631e9a9a --- /dev/null +++ b/backend/src/validation/tags.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +export const GetWorkspaceTagsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const DeleteWorkspaceTagsV2 = z.object({ + params: z.object({ + tagId: z.string().trim() + }) +}); + +export const CreateWorkspaceTagsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + name: z.string().trim(), + slug: z.string().trim() + }) +}); diff --git a/backend/src/validation/user.ts b/backend/src/validation/user.ts index f1edad3bf..9f1176f96 100644 --- a/backend/src/validation/user.ts +++ b/backend/src/validation/user.ts @@ -1,39 +1,30 @@ import fs from "fs"; import path from "path"; import { Types } from "mongoose"; -import { - IOrganization, - ISecret, - IServiceAccount, - IUser, - Membership, -} from "../models"; +import { IOrganization, ISecret, IServiceAccount, IUser, Membership } from "../models"; import { validateMembership } from "../helpers/membership"; import _ from "lodash"; import { BadRequestError, UnauthorizedRequestError, ValidationError } from "../utils/errors"; -import { - validateMembershipOrg, -} from "../helpers/membershipOrg"; -import { - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, -} from "../variables"; +import { validateMembershipOrg } from "../helpers/membershipOrg"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../variables"; +import { AuthMethod } from "../models"; +import { z } from "zod"; /** * Validate that email [email] is not disposable * @param email - email to validate */ export const validateUserEmail = (email: string) => { - const emailDomain = email.split("@")[1]; - const disposableEmails = fs.readFileSync( - path.resolve(__dirname, "../data/" + "disposable_emails.txt"), - "utf8" - ).split("\n"); - - if (disposableEmails.includes(emailDomain)) throw ValidationError({ - message: "Failed to validate email as non-disposable", - }); -} + const emailDomain = email.split("@")[1]; + const disposableEmails = fs + .readFileSync(path.resolve(__dirname, "../data/" + "disposable_emails.txt"), "utf8") + .split("\n"); + + if (disposableEmails.includes(emailDomain)) + throw ValidationError({ + message: "Failed to validate email as non-disposable" + }); +}; /** * Validate that user (client) can access workspace @@ -46,48 +37,53 @@ export const validateUserEmail = (email: string) => { * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForWorkspace = async ({ - user, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, + user, + workspaceId, + environment, + acceptedRoles, + requiredPermissions }: { - user: IUser; - workspaceId: Types.ObjectId; - environment?: string; - acceptedRoles: Array<"admin" | "member">; - requiredPermissions?: string[]; + user: IUser; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<"admin" | "member">; + requiredPermissions?: string[]; }) => { - - // validate user membership in workspace - const membership = await validateMembership({ - userId: user._id, - workspaceId, - acceptedRoles, - }); - - let runningIsDisallowed = false; - requiredPermissions?.forEach((requiredPermission: string) => { - switch (requiredPermission) { - case PERMISSION_READ_SECRETS: - runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); - break; - case PERMISSION_WRITE_SECRETS: - runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); - break; - default: - break; - } - - if (runningIsDisallowed) { - throw UnauthorizedRequestError({ - message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`, - }); - } - }); - - return membership; -} + // validate user membership in workspace + const membership = await validateMembership({ + userId: user._id, + workspaceId, + acceptedRoles + }); + + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { + environmentSlug: environment, + ability: PERMISSION_READ_SECRETS + }); + break; + case PERMISSION_WRITE_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { + environmentSlug: environment, + ability: PERMISSION_WRITE_SECRETS + }); + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + + return membership; +}; /** * Validate that user (client) can access secret [secret] @@ -98,32 +94,35 @@ export const validateUserClientForWorkspace = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForSecret = async ({ - user, - secret, - acceptedRoles, - requiredPermissions, + user, + secret, + acceptedRoles, + requiredPermissions }: { - user: IUser; - secret: ISecret; - acceptedRoles?: Array<"admin" | "member">; - requiredPermissions?: string[]; + user: IUser; + secret: ISecret; + acceptedRoles?: Array<"admin" | "member">; + requiredPermissions?: string[]; }) => { - const membership = await validateMembership({ - userId: user._id, - workspaceId: secret.workspace, - acceptedRoles, - }); - - if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { - const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + const membership = await validateMembership({ + userId: user._id, + workspaceId: secret.workspace, + acceptedRoles + }); - if (isDisallowed) { - throw UnauthorizedRequestError({ - message: "You do not have the required permissions to perform this action", - }); - } - } -} + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const isDisallowed = _.some(membership.deniedPermissions, { + environmentSlug: secret.environment, + ability: PERMISSION_WRITE_SECRETS + }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: "You do not have the required permissions to perform this action" + }); + } + } +}; /** * Validate that user (client) can access secrets [secrets] @@ -134,41 +133,44 @@ export const validateUserClientForSecret = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForSecrets = async ({ - user, - secrets, - requiredPermissions, + user, + secrets, + requiredPermissions }: { - user: IUser; - secrets: ISecret[]; - requiredPermissions?: string[]; + user: IUser; + secrets: ISecret[]; + requiredPermissions?: string[]; }) => { - - // TODO: add acceptedRoles? + // TODO: add acceptedRoles? - const userMemberships = await Membership.find({ user: user._id }) - const userMembershipById = _.keyBy(userMemberships, "workspace"); - const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); + const userMemberships = await Membership.find({ user: user._id }); + const userMembershipById = _.keyBy(userMemberships, "workspace"); + const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); - // for each secret check if the secret belongs to a workspace the user is a member of - secrets.forEach((secret: ISecret) => { - if (!workspaceIdsSet.has(secret.workspace.toString())) { - throw BadRequestError({ - message: "Failed authorization for the secret", - }); - } + // for each secret check if the secret belongs to a workspace the user is a member of + secrets.forEach((secret: ISecret) => { + if (!workspaceIdsSet.has(secret.workspace.toString())) { + throw BadRequestError({ + message: "Failed authorization for the secret" + }); + } - if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { - const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; - const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const deniedMembershipPermissions = + userMembershipById[secret.workspace.toString()].deniedPermissions; + const isDisallowed = _.some(deniedMembershipPermissions, { + environmentSlug: secret.environment, + ability: PERMISSION_WRITE_SECRETS + }); - if (isDisallowed) { - throw UnauthorizedRequestError({ - message: "You do not have the required permissions to perform this action", - }); - } - } - }); -} + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: "You do not have the required permissions to perform this action" + }); + } + } + }); +}; /** * Validate that user (client) can access service account [serviceAccount] @@ -179,25 +181,25 @@ export const validateUserClientForSecrets = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForServiceAccount = async ({ - user, - serviceAccount, - requiredPermissions, + user, + serviceAccount, + requiredPermissions }: { - user: IUser; - serviceAccount: IServiceAccount; - requiredPermissions?: string[]; + user: IUser; + serviceAccount: IServiceAccount; + requiredPermissions?: string[]; }) => { - if (!serviceAccount.user.equals(user._id)) { - // case: user who created service account is not the - // same user that is on the request - await validateMembershipOrg({ - userId: user._id, - organizationId: serviceAccount.organization, - acceptedRoles: [], - acceptedStatuses: [], - }); - } -} + if (!serviceAccount.user.equals(user._id)) { + // case: user who created service account is not the + // same user that is on the request + await validateMembershipOrg({ + userId: user._id, + organizationId: serviceAccount.organization, + acceptedRoles: [], + acceptedStatuses: [] + }); + } +}; /** * Validate that user (client) can access organization [organization] @@ -206,22 +208,54 @@ export const validateUserClientForServiceAccount = async ({ * @param {Organization} obj.organization - organization to validate against */ export const validateUserClientForOrganization = async ({ - user, - organization, - acceptedRoles, - acceptedStatuses, + user, + organization, + acceptedRoles, + acceptedStatuses }: { - user: IUser; - organization: IOrganization; - acceptedRoles: Array<"owner" | "admin" | "member">; - acceptedStatuses: Array<"invited" | "accepted">; + user: IUser; + organization: IOrganization; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { - const membershipOrg = await validateMembershipOrg({ - userId: user._id, - organizationId: organization._id, - acceptedRoles, - acceptedStatuses, - }); - - return membershipOrg; -} \ No newline at end of file + const membershipOrg = await validateMembershipOrg({ + userId: user._id, + organizationId: organization._id, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; +}; + +export const UpdateMyMfaEnabledV2 = z.object({ + body: z.object({ + isMfaEnabled: z.boolean() + }) +}); + +export const UpdateNameV2 = z.object({ + body: z.object({ + firstName: z.string().trim(), + lastName: z.string().trim() + }) +}); + +export const UpdateAuthMethodsV2 = z.object({ + body: z.object({ + authMethods: z.nativeEnum(AuthMethod).array().min(1) + }) +}); + +export const CreateApiKeyV2 = z.object({ + body: z.object({ + name: z.string().trim(), + expiresIn: z.number() + }) +}); + +export const DeleteApiKeyV2 = z.object({ + params: z.object({ + apiKeyDataId: z.string().trim() + }) +}); diff --git a/backend/src/validation/webhooks.ts b/backend/src/validation/webhooks.ts new file mode 100644 index 000000000..907f90633 --- /dev/null +++ b/backend/src/validation/webhooks.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const CreateWebhookV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + webhookUrl: z.string().url().trim(), + webhookSecretKey: z.string().trim().optional(), + secretPath: z.string().trim().default("/") + }) +}); + +export const UpdateWebhookV1 = z.object({ + params: z.object({ + webhookId: z.string().trim() + }), + body: z.object({ + isDisabled: z.boolean().default(false) + }) +}); + +export const TestWebhookV1 = z.object({ + params: z.object({ + webhookId: z.string().trim() + }) +}); + +export const DeleteWebhookV1 = z.object({ + params: z.object({ + webhookId: z.string().trim() + }) +}); + +export const ListWebhooksV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim().optional(), + secretPath: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index c329d0a16..6058e3e83 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -9,6 +9,7 @@ import { BotService } from "../services"; import { AuthData } from "../interfaces/middleware"; import { extractIPDetails } from "../utils/ip"; import { z } from "zod"; +import { EventType, UserAgentType } from "../ee/models"; /** * Validate authenticated clients for workspace with id [workspaceId] based @@ -122,9 +123,241 @@ export const validateClientForWorkspace = async ({ } }; -export const CreateWorkspacev1 = z.object({ +export const GetWorkspaceSecretSnapshotsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + environment: z.string().trim(), + folderId: z.string().trim().default("root"), + offset: z.number(), + limit: z.number() + }) +}); + +export const GetWorkspaceSecretSnapshotsCountV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + environment: z.string().trim(), + folderId: z.string().trim().default("root") + }) +}); + +export const RollbackWorkspaceSecretSnapshotV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environment: z.string().trim(), + folderId: z.string().trim().default("root"), + version: z.number() + }) +}); + +export const GetWorkspaceLogsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + offset: z.number(), + limit: z.number(), + sortBy: z.string().trim().optional(), + userId: z.string().trim().optional(), + actionNames: z.string().trim().optional() + }) +}); + +export const GetWorkspaceAuditLogsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + eventType: z.nativeEnum(EventType).nullable().optional(), + userAgentType: z.nativeEnum(UserAgentType).nullable().optional(), + startDate: z.string().datetime().nullable().optional(), + endDate: z.string().datetime().nullable().optional(), + offset: z.number(), + limit: z.number(), + actor: z.string().nullish().optional() + }) +}); + +export const GetWorkspaceAuditLogActorFilterOptsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceTrustedIpsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const AddWorkspaceTrustedIpV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + ipAddress: z.string().trim(), + comment: z.string().trim().default(""), + isActive: z.boolean() + }) +}); + +export const UpdateWorkspaceTrustedIpV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + trustedIpId: z.string().trim() + }), + body: z.object({ + ipAddress: z.string().trim(), + comment: z.string().trim().default("") + }) +}); + +export const DeleteWorkspaceTrustedIpV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + trustedIpId: z.string().trim() + }) +}); + +export const GetWorkspacePublicKeysV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceMembershipsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const CreateWorkspaceV1 = z.object({ body: z.object({ workspaceName: z.string().trim(), organizationId: z.string().trim() }) }); + +export const DeleteWorkspaceV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const ChangeWorkspaceNameV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + name: z.string().trim() + }) +}); + +export const InviteUserToWorkspaceV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + email: z.string().trim() + }) +}); + +export const GetWorkspaceIntegrationsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceIntegrationAuthorizationsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceServiceTokensV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceServiceTokenDataV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceKeyV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceMembershipsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const UpdateWorkspaceMembershipsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + membershipId: z.string().trim() + }), + body: z.object({ + role: z.string().trim() + }) +}); + +export const DeleteWorkspaceMembershipsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + membershipId: z.string().trim() + }) +}); + +export const ToggleAutoCapitalizationV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + autoCapitalization: z.string().trim() + }) +}); + +export const GetWorkspaceBlinkIndexStatusV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceSecretsV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const NameWorkspaceSecretsV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + secretsToUpdate: z + .object({ + secretName: z.string().trim(), + _id: z.string().trim() + }) + .array() + }) +});