diff --git a/backend/package-lock.json b/backend/package-lock.json index 1423cf03e..f5c55cf56 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,8 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-secrets-manager": "^3.319.0", + "@casl/ability": "^6.5.0", + "@casl/mongoose": "^7.2.1", "@godaddy/terminus": "^4.12.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^19.0.5", @@ -17,6 +19,7 @@ "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "@ucast/mongo2js": "^1.3.4", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -59,7 +62,8 @@ "typescript": "^4.9.3", "utility-types": "^3.10.0", "winston": "^3.8.2", - "winston-loki": "^6.0.7" + "winston-loki": "^6.0.6", + "zod": "^3.21.4" }, "devDependencies": { "@jest/globals": "^29.3.1", @@ -3339,6 +3343,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, + "node_modules/@casl/mongoose": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz", + "integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==", + "peerDependencies": { + "@casl/ability": "^6.3.2", + "mongoose": "^6.0.13 || ^7.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -6174,6 +6198,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -16626,6 +16681,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -19324,6 +19387,20 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "requires": { + "@ucast/mongo2js": "^1.3.0" + } + }, + "@casl/mongoose": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz", + "integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==", + "requires": {} + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -21575,6 +21652,37 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "requires": { + "@ucast/core": "^1.0.0" + } + }, + "@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "requires": { + "@ucast/core": "^1.4.1" + } + }, + "@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "requires": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -29273,6 +29381,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/backend/package.json b/backend/package.json index a70ea9974..08db10ecc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,8 @@ { "dependencies": { "@aws-sdk/client-secrets-manager": "^3.319.0", + "@casl/ability": "^6.5.0", + "@casl/mongoose": "^7.2.1", "@godaddy/terminus": "^4.12.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^19.0.5", @@ -8,6 +10,7 @@ "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "@ucast/mongo2js": "^1.3.4", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -50,7 +53,8 @@ "typescript": "^4.9.3", "utility-types": "^3.10.0", "winston": "^3.8.2", - "winston-loki": "^6.0.7" + "winston-loki": "^6.0.6", + "zod": "^3.21.4" }, "name": "infisical-api", "version": "1.0.0", diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 14c717e38..6f6411541 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"); 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/index.ts b/backend/src/controllers/v1/index.ts index 422aaa5ec..f3212d32d 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -16,6 +16,7 @@ import * as workspaceController from "./workspaceController"; import * as secretScanningController from "./secretScanningController"; import * as webhookController from "./webhookController"; import * as secretImportController from "./secretImportController"; +import * as roleController from "./roleController"; export { authController, @@ -35,5 +36,6 @@ export { workspaceController, secretScanningController, webhookController, + roleController, secretImportController }; 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..8b18fcb17 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, @@ -221,14 +275,22 @@ export const deleteIntegration = async (req: Request, res: Response) => { }); }; -// Will trigger sync for all integrations within the given env and workspace id +// Will trigger sync for all integrations within the given env and workspace id export const manualSync = async (req: Request, res: Response) => { - const { workspaceId, environment } = req.body; + const { + body: { workspaceId, environment } + } = await validateRequest(reqValidator.ManualSyncV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Integrations + ); + syncSecretsToActiveIntegrationsQueue({ workspaceId, environment - }) + }); - res.status(200).send() + res.status(200).send(); }; - 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..bc2dc0e8d 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,12 +1,23 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { IUser, Key, Membership, MembershipOrg, User } from "../../models"; +import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models"; import { EventType } from "../../ee/models"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { sendMail } from "../../helpers/nodemailer"; -import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; +import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } 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"; +import Role from "../../models/role"; +import { BadRequestError } from "../../utils/errors"; +import { InviteUserToWorkspaceV1 } from "../../validation/workspace"; /** * Check that user is a member of workspace with id [workspaceId] @@ -15,7 +26,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 +52,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 +65,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 +89,7 @@ export const deleteMembership = async (req: Request, res: Response) => { } }, { - workspaceId: membership.workspace + workspaceId: membershipToDelete.workspace } ); @@ -96,43 +105,56 @@ 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; - - if (![ADMIN, MEMBER].includes(role)) { - throw new Error("Failed to validate role"); - } + const { + body: { role }, + params: { membershipId } + } = await validateRequest(reqValidator.ChangeMembershipRoleV1, req); // 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"); + const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + if (isCustomRole) { + const wsRole = await Role.findOne({ + slug: role, + isOrgRole: false, + workspace: membershipToChangeRole.workspace + }); + if (!wsRole) throw BadRequestError({ message: "Role not found" }); + const membership = await Membership.findByIdAndUpdate(membershipId, { + role: CUSTOM, + customRole: wsRole + }); + return res.status(200).send({ + 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; + const membership = await Membership.findByIdAndUpdate( + membershipId, + { + role + }, + { + new: true + } + ); - membershipToChangeRole.role = role; - await membershipToChangeRole.save(); - await EEAuditLogService.createAuditLog( req.authData, { @@ -140,8 +162,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => { metadata: { userId: membershipToChangeRole.user._id.toString(), email: membershipToChangeRole.user.email, - oldRole, - newRole: membershipToChangeRole.role + oldRole: membershipToChangeRole.role, + newRole: role } }, { @@ -150,7 +172,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => { ); return res.status(200).send({ - membership: membershipToChangeRole + membership }); }; @@ -161,8 +183,15 @@ export const changeMembershipRole = async (req: Request, res: Response) => { * @returns */ export const inviteUserToWorkspace = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { email }: { email: string } = req.body; + const { + params: { workspaceId }, + body: { email } + } = await validateRequest(InviteUserToWorkspaceV1, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Member + ); const invitee = await User.findOne({ email @@ -179,11 +208,13 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw new Error("Failed to find workspace"); // validate invitee's organization membership - ensure that only // (accepted) organization members can be added to the workspace const membershipOrg = await MembershipOrg.findOne({ user: invitee._id, - organization: req.membership.workspace.organization, + organization: workspace.organization, status: ACCEPTED }); @@ -211,7 +242,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { substitutions: { inviterFirstName: req.user.firstName, inviterEmail: req.user.email, - workspaceName: req.membership.workspace.name, + workspaceName: workspace.name, callback_url: (await getSiteURL()) + "/login" } }); diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 0c319fe62..6e32fd662 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -8,14 +8,8 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELicenseService } from "../../ee/services"; -import { - ACCEPTED, - ADMIN, - INVITED, - MEMBER, - OWNER, - TOKEN_EMAIL_ORG_INVITATION -} from "../../variables"; +import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables"; +import * as reqValidator from "../../validation/membershipOrg"; import { getJwtSignupLifetime, getJwtSignupSecret, @@ -23,6 +17,13 @@ import { getSmtpConfigured } from "../../config"; import { validateUserEmail } from "../../validation"; +import { validateRequest } from "../../helpers/validation"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Delete organization membership with id [membershipOrgId] from organization @@ -31,7 +32,9 @@ import { validateUserEmail } from "../../validation"; * @returns */ export const deleteMembershipOrg = async (req: Request, _res: Response) => { - const { membershipOrgId } = req.params; + const { + params: { membershipOrgId } + } = await validateRequest(reqValidator.DelOrgMembershipv1, req); // check if organization membership to delete exists const membershipOrgToDelete = await MembershipOrg.findOne({ @@ -42,21 +45,14 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => { throw new Error("Failed to delete organization membership that doesn't exist"); } - // check if user is a member and admin of the organization - // whose membership we wish to delete - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: membershipOrgToDelete.organization - }); - - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); - } - - if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { - // user is not an admin member of the organization - throw new Error("Insufficient role for deleting organization membership"); - } + const { permission, membership: membershipOrg } = await getUserOrgPermissions( + req.user._id, + membershipOrgToDelete.organization.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Member + ); // delete organization membership await deleteMemberFromOrg({ @@ -96,22 +92,20 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { */ export const inviteUserToOrganization = async (req: Request, res: Response) => { let inviteeMembershipOrg, completeInviteLink; - const { organizationId, inviteeEmail } = req.body; + const { + body: { inviteeEmail, organizationId } + } = await validateRequest(reqValidator.InviteUserToOrgv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Member + ); + const host = req.headers.host; const siteUrl = `${req.protocol}://${host}`; - - // validate membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: new Types.ObjectId(organizationId) - }); - - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); - } - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - + const ssoConfig = await SSOConfig.findOne({ organization: new Types.ObjectId(organizationId) }); @@ -119,9 +113,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (ssoConfig && ssoConfig.isActive) { // case: SAML SSO is enabled for the organization return res.status(400).send({ - message: - "Failed to invite member due to SAML SSO configured for organization" - }); + message: "Failed to invite member due to SAML SSO configured for organization" + }); } if (plan.memberLimit !== null) { @@ -231,7 +224,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { */ export const verifyUserToOrganization = async (req: Request, res: Response) => { let user; - const { email, organizationId, code } = req.body; + + const { + body: { organizationId, email, code } + } = await validateRequest(reqValidator.VerifyUserToOrgv1, req); user = await User.findOne({ email }).select("+publicKey"); diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index f738891a2..3386c8767 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -1,28 +1,37 @@ import { Request, Response } from "express"; import { - IncidentContactOrg, - Membership, - MembershipOrg, - Organization, - Workspace, + IncidentContactOrg, + Membership, + MembershipOrg, + Organization, + Workspace } from "../../models"; import { createOrganization as create } from "../../helpers/organization"; import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { ACCEPTED, OWNER } from "../../variables"; import { getLicenseServerUrl, getSiteURL } from "../../config"; import { licenseServerKeyRequest } from "../../config/request"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/organization"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../services/RoleService"; +import { OrganizationNotFoundError } from "../../utils/errors"; +import { ForbiddenError } from "@casl/ability"; export const getOrganizations = async (req: Request, res: Response) => { const organizations = ( await MembershipOrg.find({ user: req.user._id, - status: ACCEPTED, + status: ACCEPTED }).populate("organization") ).map((m) => m.organization); - return res.status(200).send({ - organizations, - }); + return res.status(200).send({ + organizations + }); }; /** @@ -33,28 +42,26 @@ export const getOrganizations = async (req: Request, res: Response) => { * @returns */ export const createOrganization = async (req: Request, res: Response) => { - const { organizationName } = req.body; - - if (organizationName.length < 1) { - throw new Error("Organization names must be at least 1-character long"); - } + const { + body: { organizationName } + } = await validateRequest(reqValidator.CreateOrgv1, req); // create organization and add user as member const organization = await create({ email: req.user.email, - name: organizationName, + name: organizationName }); await addMembershipsOrg({ userIds: [req.user._id.toString()], organizationId: organization._id.toString(), roles: [OWNER], - statuses: [ACCEPTED], + statuses: [ACCEPTED] }); - return res.status(200).send({ - organization, - }); + return res.status(200).send({ + organization + }); }; /** @@ -64,10 +71,23 @@ export const createOrganization = async (req: Request, res: Response) => { * @returns */ export const getOrganization = async (req: Request, res: Response) => { - const organization = req.organization - return res.status(200).send({ - organization, - }); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgv1, req); + + // ensure user has membership + await getUserOrgPermissions(req.user._id, organizationId); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + return res.status(200).send({ + organization + }); }; /** @@ -77,15 +97,23 @@ export const getOrganization = async (req: Request, res: Response) => { * @returns */ export const getOrganizationMembers = async (req: Request, res: Response) => { - const { organizationId } = req.params; + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgMembersv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Member + ); const users = await MembershipOrg.find({ - organization: organizationId, + organization: organizationId }).populate("user", "+publicKey"); - return res.status(200).send({ - users, - }); + return res.status(200).send({ + users + }); }; /** @@ -94,17 +122,22 @@ export const getOrganizationMembers = async (req: Request, res: Response) => { * @param res * @returns */ -export const getOrganizationWorkspaces = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; +export const getOrganizationWorkspaces = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgWorkspacesv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Workspace + ); const workspacesSet = new Set( ( await Workspace.find( { - organization: organizationId, + organization: organizationId }, "_id" ) @@ -113,15 +146,15 @@ export const getOrganizationWorkspaces = async ( const workspaces = ( await Membership.find({ - user: req.user._id, + user: req.user._id }).populate("workspace") ) .filter((m) => workspacesSet.has(m.workspace._id.toString())) .map((m) => m.workspace); - return res.status(200).send({ - workspaces, - }); + return res.status(200).send({ + workspaces + }); }; /** @@ -131,25 +164,33 @@ export const getOrganizationWorkspaces = async ( * @returns */ export const changeOrganizationName = async (req: Request, res: Response) => { - const { organizationId } = req.params; - const { name } = req.body; + const { + params: { organizationId }, + body: { name } + } = await validateRequest(reqValidator.ChangeOrgNamev1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Settings + ); const organization = await Organization.findOneAndUpdate( { - _id: organizationId, + _id: organizationId }, { - name, + name }, { - new: true, + new: true } ); - return res.status(200).send({ - message: "Successfully changed organization name", - organization, - }); + return res.status(200).send({ + message: "Successfully changed organization name", + organization + }); }; /** @@ -158,19 +199,24 @@ export const changeOrganizationName = async (req: Request, res: Response) => { * @param res * @returns */ -export const getOrganizationIncidentContacts = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; +export const getOrganizationIncidentContacts = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgIncidentContactv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.IncidentAccount + ); const incidentContactsOrg = await IncidentContactOrg.find({ - organization: organizationId, + organization: organizationId }); - return res.status(200).send({ - incidentContactsOrg, - }); + return res.status(200).send({ + incidentContactsOrg + }); }; /** @@ -179,12 +225,17 @@ export const getOrganizationIncidentContacts = async ( * @param res * @returns */ -export const addOrganizationIncidentContact = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; - const { email } = req.body; +export const addOrganizationIncidentContact = async (req: Request, res: Response) => { + const { + params: { organizationId }, + body: { email } + } = await validateRequest(reqValidator.CreateOrgIncideContact, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.IncidentAccount + ); const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( { email, organization: organizationId }, @@ -192,9 +243,9 @@ export const addOrganizationIncidentContact = async ( { upsert: true, new: true } ); - return res.status(200).send({ - incidentContactOrg, - }); + return res.status(200).send({ + incidentContactOrg + }); }; /** @@ -203,22 +254,27 @@ export const addOrganizationIncidentContact = async ( * @param res * @returns */ -export const deleteOrganizationIncidentContact = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; - const { email } = req.body; +export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => { + const { + params: { organizationId }, + body: { email } + } = await validateRequest(reqValidator.DelOrgIncideContact, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.IncidentAccount + ); const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ email, - organization: organizationId, + organization: organizationId }); - return res.status(200).send({ - message: "Successfully deleted organization incident contact", - incidentContactOrg, - }); + return res.status(200).send({ + message: "Successfully deleted organization incident contact", + incidentContactOrg + }); }; /** @@ -228,19 +284,41 @@ export const deleteOrganizationIncidentContact = async ( * @param res * @returns */ -export const createOrganizationPortalSession = async ( - req: Request, - res: Response -) => { - const { data: { pmtMethods } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, +export const createOrganizationPortalSession = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Billing ); - + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { pmtMethods } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods` + ); + if (pmtMethods.length < 1) { // case: organization has no payment method on file - // -> redirect to add payment method portal - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, + // -> redirect to add payment method portal + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods`, { success_url: (await getSiteURL()) + "/dashboard", cancel_url: (await getSiteURL()) + "/dashboard" @@ -250,8 +328,12 @@ export const createOrganizationPortalSession = async ( } else { // case: organization has payment method on file // -> redirect to billing portal - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/billing-portal`, + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/billing-portal`, { return_url: (await getSiteURL()) + "/dashboard" } @@ -266,36 +348,43 @@ export const createOrganizationPortalSession = async ( * @param res * @returns */ -export const getOrganizationMembersAndTheirWorkspaces = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; +export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgMembersv1, req); - const workspacesSet = ( - await Workspace.find( - { - organization: organizationId, - }, - "_id" - ) - ).map((w) => w._id.toString()); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Member + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Workspace + ); - const memberships = ( - await Membership.find({ - workspace: { $in: workspacesSet }, - }).populate("workspace") - ); - const userToWorkspaceIds: any = {}; + const workspacesSet = ( + await Workspace.find( + { + organization: organizationId + }, + "_id" + ) + ).map((w) => w._id.toString()); - memberships.forEach(membership => { - const user = membership.user.toString(); - if (userToWorkspaceIds[user]) { - userToWorkspaceIds[user].push(membership.workspace); - } else { - userToWorkspaceIds[user] = [membership.workspace]; - } - }); + const memberships = await Membership.find({ + workspace: { $in: workspacesSet } + }).populate("workspace"); + const userToWorkspaceIds: any = {}; - return res.json(userToWorkspaceIds); + memberships.forEach((membership) => { + const user = membership.user.toString(); + if (userToWorkspaceIds[user]) { + userToWorkspaceIds[user].push(membership.workspace); + } else { + userToWorkspaceIds[user] = [membership.workspace]; + } + }); + + return res.json(userToWorkspaceIds); }; 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/roleController.ts b/backend/src/controllers/v1/roleController.ts new file mode 100644 index 000000000..77ff7cadf --- /dev/null +++ b/backend/src/controllers/v1/roleController.ts @@ -0,0 +1,247 @@ +import { Request, Response } from "express"; +import { + CreateRoleSchema, + DeleteRoleSchema, + GetRoleSchema, + GetUserPermission, + GetUserProjectPermission, + UpdateRoleSchema +} from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + adminProjectPermissions, + getUserProjectPermissions, + memberProjectPermissions, + viewerProjectPermission +} from "../../services/ProjectRoleService"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + adminPermissions, + getUserOrgPermissions, + memberPermissions +} from "../../services/RoleService"; +import { BadRequestError } from "../../utils/errors"; +import Role from "../../models/role"; +import { validateRequest } from "../../helpers/validation"; +import { packRules } from "@casl/ability/extra"; + +export const createRole = async (req: Request, res: Response) => { + const { + body: { workspaceId, name, description, slug, permissions, orgId } + } = await validateRequest(CreateRoleSchema, req); + + const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, orgId); + if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "user doesn't have the permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the permission." }); + } + } + + const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug }); + if (existingRole) { + throw BadRequestError({ message: "Role already exist" }); + } + + const role = new Role({ + organization: orgId, + workspace: workspaceId, + isOrgRole, + name, + slug, + permissions, + description + }); + await role.save(); + + res.status(200).json({ + message: "Successfully created role", + data: { + role + } + }); +}; + +export const updateRole = async (req: Request, res: Response) => { + const { + params: { id }, + body: { name, description, slug, permissions, workspaceId, orgId } + } = await validateRequest(UpdateRoleSchema, req); + const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule + + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, orgId); + if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "User doesn't have the org permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the workspace permission." }); + } + } + + if (slug) { + const existingRole = await Role.findOne({ + organization: orgId, + slug, + isOrgRole, + workspace: workspaceId + }); + if (existingRole && existingRole.id !== id) { + throw BadRequestError({ message: "Role already exist" }); + } + } + + const role = await Role.findByIdAndUpdate( + id, + { name, description, slug, permissions }, + { returnDocument: "after" } + ); + + if (!role) { + throw BadRequestError({ message: "Role not found" }); + } + res.status(200).json({ + message: "Successfully updated role", + data: { + role + } + }); +}; + +export const deleteRole = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(DeleteRoleSchema, req); + + const role = await Role.findById(id); + if (!role) { + throw BadRequestError({ message: "Role not found" }); + } + + const isOrgRole = !role.workspace; + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, role.organization.toString()); + if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "User doesn't have the org permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, role.workspace.toString()); + if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the workspace permission." }); + } + } + + await Role.findByIdAndDelete(role.id); + + res.status(200).json({ + message: "Successfully deleted role", + data: { + role + } + }); +}; + +export const getRoles = async (req: Request, res: Response) => { + const { + query: { workspaceId, orgId } + } = await validateRequest(GetRoleSchema, req); + + const isOrgRole = !workspaceId; + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, orgId); + if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "User doesn't have the org permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the workspace permission." }); + } + } + + const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId }); + // as this is shared between org and workspace switch the rule set based on it + const roles = [ + // owner is only in org level role + ...(isOrgRole + ? [ + { + _id: "owner", + name: "Owner", + slug: "owner", + description: "Complete administration access over the organization.", + permissions: adminPermissions.rules + } + ] + : []), + { + _id: "admin", + name: "Admin", + slug: "admin", + description: "Complete administration access over the organization", + permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules + }, + { + _id: "member", + name: "Member", + slug: "member", + description: "Non-administrative role in an organization", + permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules + }, + // viewer role only for project level + ...(isOrgRole + ? [] + : [ + { + _id: "viewer", + name: "Viewer", + slug: "viewer", + description: "Non-administrative role in an organization", + permissions: viewerProjectPermission.rules + } + ]), + ...customRoles + ]; + + res.status(200).json({ + message: "Successfully fetched role list", + data: { + roles + } + }); +}; + +export const getUserPermissions = async (req: Request, res: Response) => { + const { + params: { orgId } + } = await validateRequest(GetUserPermission, req); + const { permission } = await getUserOrgPermissions(req.user._id, orgId); + + res.status(200).json({ + data: { + permissions: packRules(permission.rules) + } + }); +}; + +export const getUserWorkspacePermissions = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(GetUserProjectPermission, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + res.status(200).json({ + data: { + permissions: packRules(permission.rules) + } + }); +}; diff --git a/backend/src/controllers/v1/secretImportController.ts b/backend/src/controllers/v1/secretImportController.ts index 23be98096..b912c0653 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 } }, { @@ -255,7 +275,9 @@ export const deleteSecretImport = async (req: Request, res: Response) => { }; export const getSecretImports = async (req: Request, res: Response) => { - const { workspaceId, environment, folderId } = req.query; + const { + query: { workspaceId, environment, folderId } + } = await validateRequest(reqValidator.GetSecretImportsV1, req); const importSecDoc = await SecretImport.findOne({ workspace: workspaceId, environment, @@ -266,19 +288,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,17 +309,28 @@ 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 }); }; export const getAllSecretsFromImport = async (req: Request, res: Response) => { - const { workspaceId, environment, folderId } = req.query as { - workspaceId: string; - environment: string; - folderId: string; - }; + const { + query: { workspaceId, environment, folderId } + } = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req); + const importSecDoc = await SecretImport.findOne({ workspace: workspaceId, environment, @@ -308,19 +341,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 +362,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/secretScanningController.ts b/backend/src/controllers/v1/secretScanningController.ts index ee55a4fc5..df2fe173a 100644 --- a/backend/src/controllers/v1/secretScanningController.ts +++ b/backend/src/controllers/v1/secretScanningController.ts @@ -2,20 +2,45 @@ import { Request, Response } from "express"; import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession"; import crypto from "crypto"; import { Types } from "mongoose"; -import { UnauthorizedRequestError } from "../../utils/errors"; +import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors"; import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation"; -import { MembershipOrg } from "../../models"; import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository" import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks"; import { ProbotOctokit } from "probot"; +import { Organization } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/secretScanning"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; export const createInstallationSession = async (req: Request, res: Response) => { const sessionId = crypto.randomBytes(16).toString("hex"); + const { + params: { organizationId } + } = await validateRequest(reqValidator.CreateInstalLSessionv1, req); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.SecretScanning + ); + await GitAppInstallationSession.findByIdAndUpdate( - req.organization, + organization, { - organization: new Types.ObjectId(req.organization), + organization: organization.id, sessionId: sessionId, user: new Types.ObjectId(req.user._id) }, @@ -24,31 +49,43 @@ export const createInstallationSession = async (req: Request, res: Response) => res.send({ sessionId: sessionId - }) -} + }); +}; export const linkInstallationToOrganization = async (req: Request, res: Response) => { - const { installationId, sessionId } = req.body + const { + body: { sessionId, installationId } + } = await validateRequest(reqValidator.LinkInstallationToOrgv1, req); - const installationSession = await GitAppInstallationSession.findOneAndDelete({ sessionId: sessionId }) + const installationSession = await GitAppInstallationSession.findOneAndDelete({ + sessionId: sessionId + }); if (!installationSession) { - throw UnauthorizedRequestError() + throw UnauthorizedRequestError(); } - const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization }) - if (!userMembership) { - throw UnauthorizedRequestError() - } + const { permission } = await getUserOrgPermissions( + req.user._id, + installationSession.organization.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.SecretScanning + ); - const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({ - organizationId: installationSession.organization, - }, { - installationId: installationId, - organizationId: installationSession.organization, - user: installationSession.user - }, { - upsert: true - }).lean() + const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate( + { + organizationId: installationSession.organization + }, + { + installationId: installationId, + organizationId: installationSession.organization, + user: installationSession.user + }, + { + upsert: true + } + ).lean(); const octokit = new ProbotOctokit({ auth: { @@ -66,41 +103,68 @@ export const linkInstallationToOrganization = async (req: Request, res: Response } export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => { - const { organizationId } = req.params + const { organizationId } = req.params; try { - const appInstallation = await GitAppOrganizationInstallation.findOne({ organizationId: organizationId }).lean() + const appInstallation = await GitAppOrganizationInstallation.findOne({ + organizationId: organizationId + }).lean(); if (!appInstallation) { res.json({ appInstallationComplete: false - }) + }); } res.json({ appInstallationComplete: true - }) + }); } catch { res.json({ appInstallationComplete: false - }) + }); } -} +}; export const getRisksForOrganization = async (req: Request, res: Response) => { - const { organizationId } = req.params - const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean() + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgRisksv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.SecretScanning + ); + + const risks = await GitRisks.find({ organization: organizationId }) + .sort({ createdAt: -1 }) + .lean(); res.json({ risks: risks - }) -} + }); +}; export const updateRisksStatus = async (req: Request, res: Response) => { - const { riskId } = req.params - const { status } = req.body - const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false + const { + params: { organizationId, riskId }, + body: { status } + } = await validateRequest(reqValidator.UpdateRiskStatusv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.SecretScanning + ); + + const isRiskResolved = + status == STATUS_RESOLVED_FALSE_POSITIVE || + status == STATUS_RESOLVED_REVOKED || + status == STATUS_RESOLVED_NOT_REVOKED + ? true + : false; const risk = await GitRisks.findByIdAndUpdate(riskId, { status: status, isResolved: isRiskResolved - }).lean() + }).lean(); - res.json(risk) -} \ No newline at end of file + res.json(risk); +}; 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 1425c142c..6c017d6be 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -5,17 +5,28 @@ import { Integration, IntegrationAuth, Membership, - MembershipOrg, + Organization, ServiceToken, - Workspace, + Workspace } from "../../models"; -import { - createWorkspace as create, - deleteWorkspace as deleteWork, -} from "../../helpers/workspace"; +import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace"; import { EELicenseService } from "../../ee/services"; import { addMemberships } from "../../helpers/membership"; import { ADMIN } from "../../variables"; +import { OrganizationNotFoundError } from "../../utils/errors"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + 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] @@ -24,21 +35,29 @@ import { ADMIN } from "../../variables"; * @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({ - workspace: workspaceId, + workspace: workspaceId }).populate<{ user: IUser }>("user", "publicKey") ).map((member) => { return { publicKey: member.user.publicKey, - userId: member.user._id, + userId: member.user._id }; }); return res.status(200).send({ - publicKeys, + publicKeys }); }; @@ -49,14 +68,22 @@ 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, + workspace: workspaceId }).populate("user", "+publicKey"); return res.status(200).send({ - users, + users }); }; @@ -69,12 +96,12 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => { const workspaces = ( await Membership.find({ - user: req.user._id, + user: req.user._id }).populate("workspace") ).map((m) => m.workspace); return res.status(200).send({ - workspaces, + workspaces }); }; @@ -85,14 +112,16 @@ 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, + _id: workspaceId }); return res.status(200).send({ - workspace, + workspace }); }; @@ -104,26 +133,32 @@ 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); - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: new Types.ObjectId(organizationId), - }); - - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); } + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Workspace + ); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - + if (plan.workspaceLimit !== null) { // case: limit imposed on number of workspaces allowed if (plan.workspacesUsed >= plan.workspaceLimit) { // case: number of workspaces used exceeds the number of workspaces allowed return res.status(400).send({ - message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.", + message: + "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." }); } } @@ -135,17 +170,17 @@ export const createWorkspace = async (req: Request, res: Response) => { // create workspace and add user as member const workspace = await create({ name: workspaceName, - organizationId: new Types.ObjectId(organizationId), + organizationId: new Types.ObjectId(organizationId) }); await addMemberships({ userIds: [req.user._id], workspaceId: workspace._id.toString(), - roles: [ADMIN], + roles: [ADMIN] }); return res.status(200).send({ - workspace, + workspace }); }; @@ -156,15 +191,23 @@ export const createWorkspace = async (req: Request, res: Response) => { * @returns */ export const deleteWorkspace = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.DeleteWorkspaceV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Workspace + ); // delete workspace await deleteWork({ - id: workspaceId, + id: workspaceId }); return res.status(200).send({ - message: "Successfully deleted workspace", + message: "Successfully deleted workspace" }); }; @@ -175,24 +218,32 @@ 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( { - _id: workspaceId, + _id: workspaceId }, { - name, + name }, { - new: true, + new: true } ); return res.status(200).send({ message: "Successfully changed workspace name", - workspace, + workspace }); }; @@ -203,14 +254,21 @@ export const changeWorkspaceName = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); const integrations = await Integration.find({ - workspace: workspaceId, + workspace: workspaceId }); return res.status(200).send({ - integrations, + integrations }); }; @@ -220,18 +278,23 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { * @param res * @returns */ -export const getWorkspaceIntegrationAuthorizations = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; +export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => { + 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, + workspace: workspaceId }); return res.status(200).send({ - authorizations, + authorizations }); }; @@ -241,18 +304,24 @@ export const getWorkspaceIntegrationAuthorizations = async ( * @param res * @returns */ -export const getWorkspaceServiceTokens = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; +export const getWorkspaceServiceTokens = async (req: Request, res: Response) => { + 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, - workspace: workspaceId, + workspace: workspaceId }); return res.status(200).send({ - serviceTokens, + serviceTokens }); }; 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..1d43630db 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 + } }); }; @@ -91,34 +105,46 @@ export const createWorkspaceEnvironment = async ( * @param res * @returns */ -export const reorderWorkspaceEnvironments = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName } = req.body; +export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName } + } = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Environments + ); // atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) { - throw BadRequestError({message: "Couldn't load workspace"}); + throw BadRequestError({ message: "Couldn't load workspace" }); } - const environmentIndex = workspace.environments.findIndex((env) => env.name === environmentName && env.slug === environmentSlug) - const otherEnvironmentIndex = workspace.environments.findIndex((env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug) + const environmentIndex = workspace.environments.findIndex( + (env) => env.name === environmentName && env.slug === environmentSlug + ); + const otherEnvironmentIndex = workspace.environments.findIndex( + (env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug + ); if (environmentIndex === -1 || otherEnvironmentIndex === -1) { - throw BadRequestError({message: "environment or otherEnvironment couldn't be found"}) + throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" }); } // swap the order of the environments - [workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [workspace.environments[otherEnvironmentIndex], workspace.environments[environmentIndex]] + [workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [ + workspace.environments[otherEnvironmentIndex], + workspace.environments[environmentIndex] + ]; - await workspace.save() + await workspace.save(); return res.status(200).send({ message: "Successfully reordered environments", - workspace: workspaceId, + workspace: workspaceId }); }; @@ -129,12 +155,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 +180,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 +220,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 +247,8 @@ export const renameWorkspaceEnvironment = async ( workspace: workspaceId, environment: { name: environmentName, - slug: environmentSlug, - }, + slug: environmentSlug + } }); }; @@ -229,21 +258,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 +289,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 +312,7 @@ export const deleteWorkspaceEnvironment = async ( await Integration.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); await Membership.updateMany( { workspace: workspaceId }, @@ -305,46 +338,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/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index 301cac9d0..2044183af 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,21 +1,27 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { - Membership, - MembershipOrg, - ServiceAccount, - Workspace, -} from "../../models"; +import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models"; import { deleteMembershipOrg } from "../../helpers/membershipOrg"; import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; +import Role from "../../models/role"; +import { BadRequestError } from "../../utils/errors"; +import { CUSTOM } from "../../variables"; +import * as reqValidator from "../../validation/organization"; +import { validateRequest } from "../../helpers/validation"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Return memberships for organization with id [organizationId] - * @param req - * @param res + * @param req + * @param res */ export const getOrganizationMemberships = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return organization memberships' #swagger.description = 'Return organization memberships' @@ -48,24 +54,32 @@ export const getOrganizationMemberships = async (req: Request, res: Response) => } } */ - const { organizationId } = req.params; + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgMembersv2, req); - const memberships = await MembershipOrg.find({ - organization: organizationId, - }).populate("user", "+publicKey"); - - return res.status(200).send({ - memberships, - }); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Member + ); + + const memberships = await MembershipOrg.find({ + organization: organizationId + }).populate("user", "+publicKey"); + + return res.status(200).send({ + memberships + }); +}; /** * Update role of membership with id [membershipId] to role [role] - * @param req - * @param res + * @param req + * @param res */ export const updateOrganizationMembership = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Update organization membership' #swagger.description = 'Update organization membership' @@ -118,31 +132,53 @@ export const updateOrganizationMembership = async (req: Request, res: Response) } } */ - const { membershipId } = req.params; - const { role } = req.body; - - const membership = await MembershipOrg.findByIdAndUpdate( - membershipId, - { - role, - }, { - new: true, - } - ); - - return res.status(200).send({ - membership, + const { + params: { organizationId, membershipId }, + body: { role } + } = await validateRequest(reqValidator.UpdateOrgMemberv2, req); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Member + ); + + const isCustomRole = !["admin", "member", "owner"].includes(role); + if (isCustomRole) { + const orgRole = await Role.findOne({ slug: role, isOrgRole: true }); + if (!orgRole) throw BadRequestError({ message: "Role not found" }); + + const membership = await MembershipOrg.findByIdAndUpdate(membershipId, { + role: CUSTOM, + customRole: orgRole }); -} + return res.status(200).send({ + membership + }); + } + + const membership = await MembershipOrg.findByIdAndUpdate( + membershipId, + { + role + }, + { + new: true + } + ); + + return res.status(200).send({ + membership + }); +}; /** * Delete organization membership with id [membershipId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteOrganizationMembership = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Delete organization membership' #swagger.description = 'Delete organization membership' @@ -178,30 +214,37 @@ export const deleteOrganizationMembership = async (req: Request, res: Response) } } */ - const { membershipId } = req.params; - - // delete organization membership - const membership = await deleteMembershipOrg({ - membershipOrgId: membershipId, - }); + const { + params: { organizationId, membershipId } + } = await validateRequest(reqValidator.DeleteOrgMemberv2, req); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Member + ); - await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString(), - }); + // delete organization membership + const membership = await deleteMembershipOrg({ + membershipOrgId: membershipId + }); - return res.status(200).send({ - membership, - }); -} + await updateSubscriptionOrgQuantity({ + organizationId: membership.organization.toString() + }); + + return res.status(200).send({ + membership + }); +}; /** * Return workspaces for organization with id [organizationId] that user has * access to - * @param req - * @param res + * @param req + * @param res */ export const getOrganizationWorkspaces = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return projects in organization that user is part of' #swagger.description = 'Return projects in organization that user is part of' @@ -234,45 +277,53 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) => } } */ - const { organizationId } = req.params; + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgWorkspacesv2, req); - const workspacesSet = new Set( - ( - await Workspace.find( - { - organization: organizationId, - }, - "_id" - ) - ).map((w) => w._id.toString()) - ); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Workspace + ); - const workspaces = ( - await Membership.find({ - user: req.user._id, - }).populate("workspace") - ) + const workspacesSet = new Set( + ( + await Workspace.find( + { + organization: organizationId + }, + "_id" + ) + ).map((w) => w._id.toString()) + ); + + const workspaces = ( + await Membership.find({ + user: req.user._id + }).populate("workspace") + ) .filter((m) => workspacesSet.has(m.workspace._id.toString())) .map((m) => m.workspace); -return res.status(200).send({ - workspaces, - }); -} + return res.status(200).send({ + workspaces + }); +}; /** * Return service accounts for organization with id [organizationId] - * @param req - * @param res + * @param req + * @param res */ export const getOrganizationServiceAccounts = async (req: Request, res: Response) => { - const { organizationId } = req.params; - - const serviceAccounts = await ServiceAccount.find({ - organization: new Types.ObjectId(organizationId), - }); - - return res.status(200).send({ - serviceAccounts, - }); -} + const { organizationId } = req.params; + + const serviceAccounts = await ServiceAccount.find({ + organization: new Types.ObjectId(organizationId) + }); + + return res.status(200).send({ + serviceAccounts + }); +}; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 6c9b29203..1088152df 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -25,7 +25,6 @@ import { userHasWriteOnlyAbility } from "../../ee/helpers/checkMembershipPermissions"; import _ from "lodash"; -import { BatchSecret, BatchSecretRequest } from "../../types/secret"; import { getFolderByPath, getFolderIdFromServiceToken, @@ -35,6 +34,18 @@ import { import { isValidScope } from "../../helpers/secrets"; import path from "path"; import { getAllImportedSecrets } from "../../services/SecretImportService"; +import { validateRequest } from "../../helpers/validation"; +import { + BatchSecretsV2, + GetSecretsV2, + validateServiceTokenDataClientForWorkspace +} from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError, subject } from "@casl/ability"; /** * Peform a batch of any specified CUD secret operations @@ -46,22 +57,31 @@ 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; + body: { workspaceId, environment, requests } + } = validatedData; + let { + body: { secretPath, folderId } + } = validatedData; - let secretPath = req.body.secretPath as string; - let folderId = req.body.folderId as string; + const secretIds = requests + .filter(({ method }) => method !== "POST") + // akhilmhdh: ts is dumb + .map((el) => new Types.ObjectId((el.secret as any)._id)); - const createSecrets: BatchSecret[] = []; - const updateSecrets: BatchSecret[] = []; - const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = []; + const oldSecrets = await Secret.find({ + _id: { + $in: secretIds + } + }); + if (oldSecrets.length != secretIds.length) { + throw BadRequestError({ message: "Failed to validate non-existent secrets" }); + } + + const createSecrets: any[] = []; + const updateSecrets: any[] = []; + const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = []; const actions: IAction[] = []; // get secret blind index salt @@ -71,16 +91,6 @@ 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); - - // in service token when not giving secretpath folderid must be root - // this is to avoid giving folderid when service tokens are used - if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - if (secretPath) { folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); } @@ -94,6 +104,16 @@ export const batchSecrets = async (req: Request, res: Response) => { ); } + if (req.authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } + for await (const request of requests) { // do a validation @@ -110,7 +130,7 @@ export const batchSecrets = async (req: Request, res: Response) => { version: 1, user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, environment, - workspace: new Types.ObjectId(workspaceId), + workspace: workspaceId, folder: folderId, secretBlindIndex, algorithm: ALGORITHM_AES_256_GCM, @@ -125,7 +145,7 @@ export const batchSecrets = async (req: Request, res: Response) => { updateSecrets.push({ ...request.secret, - _id: new Types.ObjectId(request.secret._id), + _id: request.secret._id, secretBlindIndex, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, @@ -133,15 +153,39 @@ 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; } } + // not using service token using auth + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + if (createSecrets.length) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + if (updateSecrets.length) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + if (deleteSecrets.length) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } // handle create secrets let createdSecrets: ISecret[] = []; if (createSecrets.length > 0) { - createdSecrets = await Secret.insertMany(createSecrets); + createdSecrets = (await Secret.insertMany(createSecrets)) as any; // (EE) add secret versions for new secrets await EESecretService.addSecretVersions({ secretVersions: createdSecrets.map((n: any) => { @@ -206,7 +250,7 @@ export const batchSecrets = async (req: Request, res: Response) => { // handle update secrets let updatedSecrets: ISecret[] = []; - if (updateSecrets.length > 0 && req.secrets) { + if (updateSecrets.length > 0 && oldSecrets) { // construct object containing all secrets let listedSecretsObj: { [key: string]: { @@ -215,7 +259,7 @@ export const batchSecrets = async (req: Request, res: Response) => { }; } = {}; - listedSecretsObj = req.secrets.reduce( + listedSecretsObj = oldSecrets.reduce( (obj: any, secret: ISecret) => ({ ...obj, [secret._id.toString()]: secret @@ -227,7 +271,8 @@ export const batchSecrets = async (req: Request, res: Response) => { updateOne: { filter: { _id: new Types.ObjectId(u._id), - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), + environment }, update: { $inc: { @@ -241,7 +286,6 @@ export const batchSecrets = async (req: Request, res: Response) => { } } })); - await Secret.bulkWrite(updateOperations); const secretVersions = updateSecrets.map( @@ -332,23 +376,26 @@ 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: { $in: deleteSecretIds - } + }, + workspace: new Types.ObjectId(workspaceId), + environment }); await EESecretService.markDeletedSecretVersions({ @@ -781,10 +828,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 +976,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); } @@ -970,17 +1020,17 @@ export const getSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); // reduce the number of events captured - let shouldRecordK8Event = false + let shouldRecordK8Event = false; if (req.authData.userAgent == K8_USER_AGENT_NAME) { const randomNumber = Math.random(); if (randomNumber > 0.9) { - shouldRecordK8Event = true + shouldRecordK8Event = true; } } if (postHogClient) { const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; - const approximateForNoneCapturedEvents = secrets.length * 10 + const approximateForNoneCapturedEvents = secrets.length * 10; if (shouldCapture) { postHogClient.capture({ @@ -1104,10 +1154,10 @@ export const updateSecrets = async (req: Request, res: Response) => { tags, ...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + } : {}) } } 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..e2b405483 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -9,8 +9,16 @@ 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"; +import { validateServiceTokenDataClientForWorkspace } from "../../validation"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; /** * Return secrets for workspace with id [workspaceId] and environment @@ -19,30 +27,38 @@ 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 - }); } + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -56,7 +72,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 +115,26 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secret = await SecretService.getSecret({ secretName, @@ -132,8 +163,26 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -197,8 +246,26 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -211,7 +278,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 +310,30 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const { secret } = await SecretService.deleteSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -282,11 +367,25 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -296,7 +395,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 +424,26 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secret = await SecretService.getSecret({ secretName, @@ -351,23 +465,41 @@ 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, + secretPath, + environment, + metadata, + type, + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext + }, + params: { secretName } + } = 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const secret = await SecretService.createSecret({ secretName, @@ -410,20 +542,38 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const secret = await SecretService.updateSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -452,12 +602,30 @@ 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 }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } 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..79d661b58 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,173 @@ 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 + }); + } 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/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index b641de985..1811a46f3 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -3,228 +3,503 @@ import { Request, Response } from "express"; import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; import { EELicenseService } from "../../services"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/organization"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; +import { Organization } from "../../../models"; +import { OrganizationNotFoundError } from "../../../utils/errors"; export const getOrganizationPlansTable = async (req: Request, res: Response) => { - const billingCycle = req.query.billingCycle as string; - - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` - ); + const { + query: { billingCycle }, + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlansTablev1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); + + return res.status(200).send(data); +}; /** * Return the organization current plan's feature set */ export const getOrganizationPlan = async (req: Request, res: Response) => { - const { organizationId } = req.params; - const workspaceId = req.query.workspaceId as string; + const { + query: { workspaceId }, + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanv1, req); - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId), new Types.ObjectId(workspaceId)); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); - return res.status(200).send({ - plan, - }); -} + const plan = await EELicenseService.getPlan( + new Types.ObjectId(organizationId), + new Types.ObjectId(workspaceId) + ); + + return res.status(200).send({ + plan + }); +}; /** * Return checkout url for pro trial - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const startOrganizationTrial = async (req: Request, res: Response) => { - const { organizationId } = req.params; - const { success_url } = req.body; + const { + params: { organizationId }, + body: { success_url } + } = await validateRequest(reqValidator.StartOrgTrailv1, req); - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`, - { - success_url - } - ); - - EELicenseService.delPlan(new Types.ObjectId(organizationId)); - - return res.status(200).send({ - url + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Billing + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" }); -} + } + + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/session/trial`, + { + success_url + } + ); + + EELicenseService.delPlan(new Types.ObjectId(organizationId)); + + return res.status(200).send({ + url + }); +}; /** * Return the organization's current plan's billing info - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing` - ); - - return res.status(200).send(data); -} + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/cloud-plan/billing` + ); + + return res.status(200).send(data); +}; /** * Return the organization's current plan's feature table - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationPlanTable = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanTablev1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/cloud-plan/table` + ); + + return res.status(200).send(data); +}; export const getOrganizationBillingDetails = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details` + ); + + return res.status(200).send(data); +}; export const updateOrganizationBillingDetails = async (req: Request, res: Response) => { - const { - name, - email - } = req.body; + const { + params: { organizationId }, + body: { name, email } + } = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req); - const { data } = await licenseServerKeyRequest.patch( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`, - { - ...(name ? { name } : {}), - ...(email ? { email } : {}) - } - ); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Billing + ); - return res.status(200).send(data); -} + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details`, + { + ...(name ? { name } : {}), + ...(email ? { email } : {}) + } + ); + + return res.status(200).send(data); +}; /** * Return the organization's payment methods on file */ export const getOrganizationPmtMethods = async (req: Request, res: Response) => { - const { data: { pmtMethods } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req); - return res.status(200).send(pmtMethods); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { pmtMethods } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods` + ); + + return res.status(200).send(pmtMethods); +}; /** * Return URL to add payment method for organization */ export const addOrganizationPmtMethod = async (req: Request, res: Response) => { - const { - success_url, - cancel_url, - } = req.body; - - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, - { - success_url, - cancel_url, - } - ); - - return res.status(200).send({ - url, - }); -} + const { + params: { organizationId }, + body: { success_url, cancel_url } + } = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods`, + { + success_url, + cancel_url + } + ); + + return res.status(200).send({ + url + }); +}; /** * Delete payment method with id [pmtMethodId] for organization - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { - const { pmtMethodId } = req.params; + const { + params: { organizationId, pmtMethodId } + } = await validateRequest(reqValidator.DelOrgPmtMethodv1, req); - const { data } = await licenseServerKeyRequest.delete( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, - ); - - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods/${pmtMethodId}` + ); + + return res.status(200).send(data); +}; /** * Return the organization's tax ids on file */ export const getOrganizationTaxIds = async (req: Request, res: Response) => { - const { data: { tax_ids } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgTaxIdsv1, req); - return res.status(200).send(tax_ids); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { tax_ids } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/tax-ids` + ); + + return res.status(200).send(tax_ids); +}; /** * Add tax id to organization */ export const addOrganizationTaxId = async (req: Request, res: Response) => { - const { - type, - value - } = req.body; + const { + params: { organizationId }, + body: { type, value } + } = await validateRequest(reqValidator.CreateOrgTaxId, req); - const { data } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`, - { - type, - value - } - ); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Billing + ); - return res.status(200).send(data); -} + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/tax-ids`, + { + type, + value + } + ); + + return res.status(200).send(data); +}; /** * Delete tax id with id [taxId] from organization tax ids on file - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteOrganizationTaxId = async (req: Request, res: Response) => { - const { taxId } = req.params; + const { + params: { organizationId, taxId } + } = await validateRequest(reqValidator.DelOrgTaxIdv1, req); - const { data } = await licenseServerKeyRequest.delete( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`, - ); - - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/tax-ids/${taxId}` + ); + + return res.status(200).send(data); +}; /** * Return organization's invoices on file - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationInvoices = async (req: Request, res: Response) => { - const { data: { invoices } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgInvoicesv1, req); - return res.status(200).send(invoices); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { invoices } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/invoices` + ); + + return res.status(200).send(invoices); +}; /** * Return organization's licenses on file - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationLicenses = async (req: Request, res: Response) => { - const { data: { licenses } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgLicencesv1, req); - return res.status(200).send(licenses); -} \ No newline at end of file + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { licenses } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/licenses` + ); + + return res.status(200).send(licenses); +}; diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 2dcc3c2c7..58b7e6264 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,7 +1,17 @@ +import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; -import { Secret } from "../../../models"; +import { validateRequest } from "../../../helpers/validation"; +import { Folder, 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"; +import { getFolderWithPathFromId } from "../../../services/FolderService"; /** * Return secret versions for secret with id [secretId] @@ -54,10 +64,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 +147,24 @@ 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 + ); // validate secret version const oldSecretVersion = await SecretVersion.findOne({ @@ -154,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { keyEncoding } = oldSecretVersion; + let secretPath = "/"; + const folders = await Folder.findOne({ workspace, environment }); + if (folders) + secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/"; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath }) + ); + // update secret const secret = await Secret.findByIdAndUpdate( secretId, 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/ssoController.ts b/backend/src/ee/controllers/v1/ssoController.ts index d75cf25d2..bacc6befc 100644 --- a/backend/src/ee/controllers/v1/ssoController.ts +++ b/backend/src/ee/controllers/v1/ssoController.ts @@ -2,239 +2,258 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { BotOrgService } from "../../../services"; import { SSOConfig } from "../../models"; -import { - AuthMethod, - MembershipOrg, - User -} from "../../../models"; +import { AuthMethod, MembershipOrg, User } from "../../../models"; import { getSSOConfigHelper } from "../../helpers/organizations"; import { client } from "../../../config"; import { ResourceNotFoundError } from "../../../utils/errors"; import { getSiteURL } from "../../../config"; import { EELicenseService } from "../../services"; +import * as reqValidator from "../../../validation/sso"; +import { validateRequest } from "../../../helpers/validation"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Redirect user to appropriate SSO endpoint after successful authentication * to finish inputting their master key for logging in or signing up - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const redirectSSO = async (req: Request, res: Response) => { - if (req.isUserCompleted) { - return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`); - } - - return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`); -} + if (req.isUserCompleted) { + return res.redirect( + `${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}` + ); + } + + return res.redirect( + `${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}` + ); +}; /** * Return organization SAML SSO configuration - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getSSOConfig = async (req: Request, res: Response) => { - const organizationId = req.query.organizationId as string; - - const data = await getSSOConfigHelper({ - organizationId: new Types.ObjectId(organizationId) - }); + const { + query: { organizationId } + } = await validateRequest(reqValidator.GetSsoConfigv1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Sso + ); + + const data = await getSSOConfigHelper({ + organizationId: new Types.ObjectId(organizationId) + }); + + return res.status(200).send(data); +}; /** * Update organization SAML SSO configuration - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateSSOConfig = async (req: Request, res: Response) => { + const { + body: { organizationId, authProvider, isActive, entryPoint, issuer, cert } + } = await validateRequest(reqValidator.UpdateSsoConfigv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Sso + ); + + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); + + if (!plan.samlSSO) + return res.status(400).send({ + message: + "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + }); + + interface PatchUpdate { + authProvider?: string; + isActive?: boolean; + encryptedEntryPoint?: string; + entryPointIV?: string; + entryPointTag?: string; + encryptedIssuer?: string; + issuerIV?: string; + issuerTag?: string; + encryptedCert?: string; + certIV?: string; + certTag?: string; + } + + const update: PatchUpdate = {}; + + if (authProvider) { + update.authProvider = authProvider; + } + + if (isActive !== undefined) { + update.isActive = isActive; + } + + const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId)); + + if (entryPoint) { const { - organizationId, - authProvider, - isActive, - entryPoint, - issuer, - cert, - } = req.body; + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = client.encryptSymmetric(entryPoint, key); - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - - if (!plan.samlSSO) return res.status(400).send({ - message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + update.encryptedEntryPoint = encryptedEntryPoint; + update.entryPointIV = entryPointIV; + update.entryPointTag = entryPointTag; + } + + if (issuer) { + const { + ciphertext: encryptedIssuer, + iv: issuerIV, + tag: issuerTag + } = client.encryptSymmetric(issuer, key); + + update.encryptedIssuer = encryptedIssuer; + update.issuerIV = issuerIV; + update.issuerTag = issuerTag; + } + + if (cert) { + const { + ciphertext: encryptedCert, + iv: certIV, + tag: certTag + } = client.encryptSymmetric(cert, key); + + update.encryptedCert = encryptedCert; + update.certIV = certIV; + update.certTag = certTag; + } + + const ssoConfig = await SSOConfig.findOneAndUpdate( + { + organization: new Types.ObjectId(organizationId) + }, + update, + { + new: true + } + ); + + if (!ssoConfig) + throw ResourceNotFoundError({ + message: "Failed to find SSO config to update" }); - - interface PatchUpdate { - authProvider?: string; - isActive?: boolean; - encryptedEntryPoint?: string; - entryPointIV?: string; - entryPointTag?: string; - encryptedIssuer?: string; - issuerIV?: string; - issuerTag?: string; - encryptedCert?: string; - certIV?: string; - certTag?: string; - } - - const update: PatchUpdate = {}; - - if (authProvider) { - update.authProvider = authProvider; - } - - if (isActive !== undefined) { - update.isActive = isActive; - } - - const key = await BotOrgService.getSymmetricKey( - new Types.ObjectId(organizationId) - ); - - if (entryPoint) { - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = client.encryptSymmetric(entryPoint, key); - - update.encryptedEntryPoint = encryptedEntryPoint; - update.entryPointIV = entryPointIV; - update.entryPointTag = entryPointTag; - } - if (issuer) { - const { - ciphertext: encryptedIssuer, - iv: issuerIV, - tag: issuerTag - } = client.encryptSymmetric(issuer, key); - - update.encryptedIssuer = encryptedIssuer; - update.issuerIV = issuerIV; - update.issuerTag = issuerTag; - } + if (update.isActive !== undefined) { + const membershipOrgs = await MembershipOrg.find({ + organization: new Types.ObjectId(organizationId) + }).select("user"); - if (cert) { - const { - ciphertext: encryptedCert, - iv: certIV, - tag: certTag - } = client.encryptSymmetric(cert, key); - - update.encryptedCert = encryptedCert; - update.certIV = certIV; - update.certTag = certTag; - } - - const ssoConfig = await SSOConfig.findOneAndUpdate( + if (update.isActive) { + await User.updateMany( { - organization: new Types.ObjectId(organizationId) + _id: { + $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) + } }, - update, { - new: true + authMethods: [ssoConfig.authProvider] } - ); - - if (!ssoConfig) throw ResourceNotFoundError({ - message: "Failed to find SSO config to update" - }); - - if (update.isActive !== undefined) { - const membershipOrgs = await MembershipOrg.find({ - organization: new Types.ObjectId(organizationId) - }).select("user"); - - if (update.isActive) { - await User.updateMany( - { - _id: { - $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) - } - }, - { - authMethods: [ssoConfig.authProvider], - } - ); - } else { - await User.updateMany( - { - _id: { - $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) - } - }, - { - authMethods: [AuthMethod.EMAIL], - } - ); + ); + } else { + await User.updateMany( + { + _id: { + $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) + } + }, + { + authMethods: [AuthMethod.EMAIL] } + ); } - - return res.status(200).send(ssoConfig); -} + } + + return res.status(200).send(ssoConfig); +}; /** * Create organization SAML SSO configuration - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createSSOConfig = async (req: Request, res: Response) => { - const { - organizationId, - authProvider, - isActive, - entryPoint, - issuer, - cert - } = req.body; + const { + body: { organizationId, authProvider, isActive, entryPoint, issuer, cert } + } = await validateRequest(reqValidator.CreateSsoConfigv1, req); - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - - if (!plan.samlSSO) return res.status(400).send({ - message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Sso + ); + + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); + + if (!plan.samlSSO) + return res.status(400).send({ + message: + "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." }); - - const key = await BotOrgService.getSymmetricKey( - new Types.ObjectId(organizationId) - ); - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = client.encryptSymmetric(entryPoint, key); + const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId)); - const { - ciphertext: encryptedIssuer, - iv: issuerIV, - tag: issuerTag - } = client.encryptSymmetric(issuer, key); + const { + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = client.encryptSymmetric(entryPoint, key); - const { - ciphertext: encryptedCert, - iv: certIV, - tag: certTag - } = client.encryptSymmetric(cert, key); - - const ssoConfig = await new SSOConfig({ - organization: new Types.ObjectId(organizationId), - authProvider, - isActive, - encryptedEntryPoint, - entryPointIV, - entryPointTag, - encryptedIssuer, - issuerIV, - issuerTag, - encryptedCert, - certIV, - certTag - }).save(); + const { + ciphertext: encryptedIssuer, + iv: issuerIV, + tag: issuerTag + } = client.encryptSymmetric(issuer, key); - return res.status(200).send(ssoConfig); -} \ No newline at end of file + const { + ciphertext: encryptedCert, + iv: certIV, + tag: certTag + } = client.encryptSymmetric(cert, key); + + const ssoConfig = await new SSOConfig({ + organization: new Types.ObjectId(organizationId), + authProvider, + isActive, + encryptedEntryPoint, + entryPointIV, + entryPointTag, + encryptedIssuer, + issuerIV, + issuerTag, + encryptedCert, + certIV, + certTag + }).save(); + + return res.status(200).send(ssoConfig); +}; 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/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 6506d3afa..bbe5019b4 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -1,237 +1,127 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../../middleware"; -import { body, param, query } from "express-validator"; +import { requireAuth } from "../../../middleware"; import { organizationsController } from "../../controllers/v1"; -import { - ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER -} from "../../../variables"; +import { AuthMode } from "../../../variables"; router.get( - "/:organizationId/plans/table", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("billingCycle").exists().isString().isIn(["monthly", "yearly"]), - validateRequest, - organizationsController.getOrganizationPlansTable + "/:organizationId/plans/table", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlansTable ); router.get( - "/:organizationId/plan", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("workspaceId").optional().isString(), - validateRequest, - organizationsController.getOrganizationPlan + "/:organizationId/plan", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlan ); router.post( - "/:organizationId/session/trial", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("success_url").exists().trim(), - validateRequest, - organizationsController.startOrganizationTrial + "/:organizationId/session/trial", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.startOrganizationTrial ); router.get( - "/:organizationId/plan/billing", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("workspaceId").optional().isString(), - validateRequest, - organizationsController.getOrganizationPlanBillingInfo + "/:organizationId/plan/billing", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlanBillingInfo ); router.get( - "/:organizationId/plan/table", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("workspaceId").optional().isString(), - validateRequest, - organizationsController.getOrganizationPlanTable + "/:organizationId/plan/table", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlanTable ); router.get( - "/:organizationId/billing-details", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationBillingDetails + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationBillingDetails ); router.patch( - "/:organizationId/billing-details", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("email").optional().isString().trim(), - body("name").optional().isString().trim(), - validateRequest, - organizationsController.updateOrganizationBillingDetails + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.updateOrganizationBillingDetails ); router.get( - "/:organizationId/billing-details/payment-methods", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationPmtMethods + "/:organizationId/billing-details/payment-methods", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPmtMethods ); router.post( - "/:organizationId/billing-details/payment-methods", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("success_url").exists().isString(), - body("cancel_url").exists().isString(), - validateRequest, - organizationsController.addOrganizationPmtMethod + "/:organizationId/billing-details/payment-methods", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.addOrganizationPmtMethod ); router.delete( - "/:organizationId/billing-details/payment-methods/:pmtMethodId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - param("pmtMethodId").exists().trim(), - validateRequest, - organizationsController.deleteOrganizationPmtMethod + "/:organizationId/billing-details/payment-methods/:pmtMethodId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.deleteOrganizationPmtMethod ); router.get( - "/:organizationId/billing-details/tax-ids", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationTaxIds + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationTaxIds ); router.post( - "/:organizationId/billing-details/tax-ids", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("type").exists().isString(), - body("value").exists().isString(), - validateRequest, - organizationsController.addOrganizationTaxId + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.addOrganizationTaxId ); router.delete( - "/:organizationId/billing-details/tax-ids/:taxId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - param("taxId").exists().trim(), - validateRequest, - organizationsController.deleteOrganizationTaxId + "/:organizationId/billing-details/tax-ids/:taxId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.deleteOrganizationTaxId ); router.get( - "/:organizationId/invoices", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationInvoices + "/:organizationId/invoices", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationInvoices ); router.get( - "/:organizationId/licenses", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationLicenses + "/:organizationId/licenses", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationLicenses ); -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/secretScanning.ts b/backend/src/ee/routes/v1/secretScanning.ts index 1cc3adc46..0afdf0545 100644 --- a/backend/src/ee/routes/v1/secretScanning.ts +++ b/backend/src/ee/routes/v1/secretScanning.ts @@ -1,81 +1,53 @@ import express from "express"; const router = express.Router(); +import { requireAuth } from "../../../middleware"; import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../../middleware"; -import { body, param } from "express-validator"; -import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../../controllers/v1/secretScanningController"; -import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables"; + createInstallationSession, + getCurrentOrganizationInstallationStatus, + getRisksForOrganization, + linkInstallationToOrganization, + updateRisksStatus +} from "../../../controllers/v1/secretScanningController"; +import { AuthMode } from "../../../variables"; router.post( "/create-installation-session/organization/:organizationId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, createInstallationSession ); router.post( "/link-installation", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - body("installationId").exists().trim(), - body("sessionId").exists().trim(), - validateRequest, linkInstallationToOrganization ); router.get( "/installation-status/organization/:organizationId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, getCurrentOrganizationInstallationStatus ); router.get( "/organization/:organizationId/risks", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, getRisksForOrganization ); router.post( "/organization/:organizationId/risks/:riskId/status", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - param("riskId").exists().trim(), - body("status").exists(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, updateRisksStatus ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index ecfe47ca5..f8c643e60 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -1,27 +1,15 @@ import express from "express"; const router = express.Router(); -import { - requireSecretSnapshotAuth, -} from "../../middleware"; -import { - requireAuth, - validateRequest, -} from "../../../middleware"; -import { param } from "express-validator"; -import { ADMIN, AuthMode, MEMBER } from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; import { secretSnapshotController } from "../../controllers/v1"; router.get( - "/:secretSnapshotId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireSecretSnapshotAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("secretSnapshotId").exists().trim(), - validateRequest, - secretSnapshotController.getSecretSnapshot + "/:secretSnapshotId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretSnapshotController.getSecretSnapshot ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index 005f84a0e..564073aec 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -1,146 +1,95 @@ import express from "express"; const router = express.Router(); import passport from "passport"; -import { - AuthProvider -} from "../../models"; -import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../../middleware"; -import { body, query } from "express-validator"; +import { requireAuth } from "../../../middleware"; import { ssoController } from "../../controllers/v1"; import { authLimiter } from "../../../helpers/rateLimiter"; -import { - ACCEPTED, - ADMIN, - AuthMode, - OWNER -} from "../../../variables"; +import { AuthMode } from "../../../variables"; -router.get( - "/redirect/google", - authLimiter, - (req, res, next) => { - passport.authenticate("google", { - scope: ["profile", "email"], - session: false, - ...(req.query.callback_port ? { - state: req.query.callback_port as string - } : {}) - })(req, res, next); - } -); +router.get("/redirect/google", authLimiter, (req, res, next) => { + passport.authenticate("google", { + scope: ["profile", "email"], + session: false, + ...(req.query.callback_port + ? { + state: req.query.callback_port as string + } + : {}) + })(req, res, next); +}); router.get( "/google", - passport.authenticate("google", { - failureRedirect: "/login/provider/error", - session: false + passport.authenticate("google", { + failureRedirect: "/login/provider/error", + session: false }), ssoController.redirectSSO ); -router.get( - "/redirect/github", - authLimiter, - (req, res, next) => { - passport.authenticate("github", { - session: false, - ...(req.query.callback_port ? { - state: req.query.callback_port as string - } : {}) - })(req, res, next); - } -); +router.get("/redirect/github", authLimiter, (req, res, next) => { + passport.authenticate("github", { + session: false, + ...(req.query.callback_port + ? { + state: req.query.callback_port as string + } + : {}) + })(req, res, next); +}); router.get( "/github", authLimiter, - passport.authenticate("github", { - failureRedirect: "/login/provider/error", - session: false + passport.authenticate("github", { + failureRedirect: "/login/provider/error", + session: false }), ssoController.redirectSSO ); -router.get( - "/redirect/saml2/:ssoIdentifier", - authLimiter, - (req, res, next) => { - const options = { - failureRedirect: "/", - additionalParams: { - RelayState: req.query.callback_port ?? "" - }, - }; - passport.authenticate("saml", options)(req, res, next); - } -); +router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => { + const options = { + failureRedirect: "/", + additionalParams: { + RelayState: req.query.callback_port ?? "" + } + }; + passport.authenticate("saml", options)(req, res, next); +}); -router.post("/saml2/:ssoIdentifier", - passport.authenticate("saml", { - failureRedirect: "/login/provider/error", - failureFlash: true, +router.post( + "/saml2/:ssoIdentifier", + passport.authenticate("saml", { + failureRedirect: "/login/provider/error", + failureFlash: true, session: false }), ssoController.redirectSSO ); router.get( - "/config", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "query" - }), - query("organizationId").exists().trim(), - validateRequest, - ssoController.getSSOConfig + "/config", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + ssoController.getSSOConfig ); router.post( - "/config", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "body" - }), - body("organizationId").exists().trim(), - body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]), - body("isActive").exists().isBoolean(), - body("entryPoint").exists().isString(), - body("issuer").exists().isString(), - body("cert").exists().isString(), - validateRequest, - ssoController.createSSOConfig + "/config", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + ssoController.createSSOConfig ); router.patch( - "/config", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "body" - }), - body("organizationId").exists().trim(), - body("authProvider").optional().isString(), - body("isActive").optional().isBoolean(), - body("entryPoint").optional().isString(), - body("issuer").optional().isString(), - body("cert").optional().isString(), - validateRequest, - ssoController.updateSSOConfig + "/config", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + ssoController.updateSSOConfig ); -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/helpers/membership.ts b/backend/src/helpers/membership.ts index d2fcf5b17..7700c21ee 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -11,29 +11,29 @@ import { BadRequestError, MembershipNotFoundError } from "../utils/errors"; * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] */ export const validateMembership = async ({ - userId, - workspaceId, - acceptedRoles, + userId, + workspaceId, + acceptedRoles }: { userId: Types.ObjectId | string; workspaceId: Types.ObjectId | string; - acceptedRoles?: Array<"admin" | "member">; + acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">; }) => { const membership = await Membership.findOne({ user: userId, - workspace: workspaceId, + workspace: workspaceId }).populate("workspace"); if (!membership) { throw MembershipNotFoundError({ - message: "Failed to find workspace membership", + message: "Failed to find workspace membership" }); } if (acceptedRoles) { if (!acceptedRoles.includes(membership.role)) { throw BadRequestError({ - message: "Failed authorization for membership role", + message: "Failed authorization for membership role" }); } } @@ -47,7 +47,7 @@ export const validateMembership = async ({ * @return {Object} membership - membership */ export const findMembership = async (queryObj: any) => { - const membership = await Membership.findOne(queryObj); + const membership = await Membership.findOne(queryObj); return membership; }; @@ -60,9 +60,9 @@ export const findMembership = async (queryObj: any) => { * @param {String[]} obj.roles - roles of users. */ export const addMemberships = async ({ - userIds, - workspaceId, - roles, + userIds, + workspaceId, + roles }: { userIds: string[]; workspaceId: string; @@ -74,15 +74,15 @@ export const addMemberships = async ({ filter: { user: userId, workspace: workspaceId, - role: roles[idx], + role: roles[idx] }, update: { user: userId, workspace: workspaceId, - role: roles[idx], + role: roles[idx] }, - upsert: true, - }, + upsert: true + } }; }); await Membership.bulkWrite(operations as any); @@ -94,8 +94,8 @@ export const addMemberships = async ({ * @param {String} obj.membershipId - id of membership to delete */ export const deleteMembership = async ({ membershipId }: { membershipId: string }) => { - const deletedMembership = await Membership.findOneAndDelete({ - _id: membershipId, + const deletedMembership = await Membership.findOneAndDelete({ + _id: membershipId }); // delete keys associated with the membership @@ -103,9 +103,9 @@ export const deleteMembership = async ({ membershipId }: { membershipId: string // case: membership had a registered user await Key.deleteMany({ receiver: deletedMembership.user, - workspace: deletedMembership.workspace, + workspace: deletedMembership.workspace }); } - return deletedMembership; + return deletedMembership; }; diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index 3ed5be088..46f5fbf56 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -1,14 +1,6 @@ import { Types } from "mongoose"; -import { - Key, - Membership, - MembershipOrg, - Workspace, -} from "../models"; -import { - MembershipOrgNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { Key, Membership, MembershipOrg, Workspace } from "../models"; +import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors"; /** * Validate that user with id [userId] is a member of organization with id [organizationId] @@ -19,39 +11,43 @@ import { * @param {String[]} obj.acceptedRoles */ export const validateMembershipOrg = async ({ - userId, - organizationId, - acceptedRoles, - acceptedStatuses, + userId, + organizationId, + acceptedRoles, + acceptedStatuses }: { - userId: Types.ObjectId; - organizationId: Types.ObjectId; - acceptedRoles?: Array<"owner" | "admin" | "member">; - acceptedStatuses?: Array<"invited" | "accepted">; + userId: Types.ObjectId; + organizationId: Types.ObjectId; + acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">; + acceptedStatuses?: Array<"invited" | "accepted">; }) => { - const membershipOrg = await MembershipOrg.findOne({ - user: userId, - organization: organizationId, - }); - - if (!membershipOrg) { - throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); - } - - if (acceptedRoles) { - if (!acceptedRoles.includes(membershipOrg.role)) { - throw UnauthorizedRequestError({ message: "Failed to validate organization membership role" }); - } - } - - if (acceptedStatuses) { - if (!acceptedStatuses.includes(membershipOrg.status)) { - throw UnauthorizedRequestError({ message: "Failed to validate organization membership status" }); - } - } - - return membershipOrg; -} + const membershipOrg = await MembershipOrg.findOne({ + user: userId, + organization: organizationId + }); + + if (!membershipOrg) { + throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); + } + + if (acceptedRoles) { + if (!acceptedRoles.includes(membershipOrg.role)) { + throw UnauthorizedRequestError({ + message: "Failed to validate organization membership role" + }); + } + } + + if (acceptedStatuses) { + if (!acceptedStatuses.includes(membershipOrg.status)) { + throw UnauthorizedRequestError({ + message: "Failed to validate organization membership status" + }); + } + } + + return membershipOrg; +}; /** * Return organization membership matching criteria specified in @@ -60,8 +56,8 @@ export const validateMembershipOrg = async ({ * @return {Object} membershipOrg - membership */ export const findMembershipOrg = (queryObj: any) => { - const membershipOrg = MembershipOrg.findOne(queryObj); - return membershipOrg; + const membershipOrg = MembershipOrg.findOne(queryObj); + return membershipOrg; }; /** @@ -73,15 +69,15 @@ export const findMembershipOrg = (queryObj: any) => { * @param {String[]} obj.roles - roles of users. */ export const addMembershipsOrg = async ({ - userIds, - organizationId, - roles, - statuses, + userIds, + organizationId, + roles, + statuses }: { - userIds: string[]; - organizationId: string; - roles: string[]; - statuses: string[]; + userIds: string[]; + organizationId: string; + roles: string[]; + statuses: string[]; }) => { const operations = userIds.map((userId, idx) => { return { @@ -90,16 +86,16 @@ export const addMembershipsOrg = async ({ user: userId, organization: organizationId, role: roles[idx], - status: statuses[idx], + status: statuses[idx] }, update: { user: userId, organization: organizationId, role: roles[idx], - status: statuses[idx], + status: statuses[idx] }, - upsert: true, - }, + upsert: true + } }; }); @@ -111,13 +107,9 @@ export const addMembershipsOrg = async ({ * @param {Object} obj * @param {String} obj.membershipOrgId - id of organization membership to delete */ -export const deleteMembershipOrg = async ({ - membershipOrgId, -}: { - membershipOrgId: string; -}) => { +export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string }) => { const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ - _id: membershipOrgId, + _id: membershipOrgId }); if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership"); @@ -128,24 +120,24 @@ export const deleteMembershipOrg = async ({ const workspaces = ( await Workspace.find({ - organization: deletedMembershipOrg.organization, + organization: deletedMembershipOrg.organization }) ).map((w) => w._id.toString()); await Membership.deleteMany({ user: deletedMembershipOrg.user, workspace: { - $in: workspaces, - }, + $in: workspaces + } }); await Key.deleteMany({ receiver: deletedMembershipOrg.user, workspace: { - $in: workspaces, - }, + $in: workspaces + } }); } - return deletedMembershipOrg; -}; \ No newline at end of file + return deletedMembershipOrg; +}; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index ad6296adf..510da7dd9 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -504,11 +504,6 @@ export const getSecretsHelper = async ({ }: GetSecretsParams) => { let secrets: ISecret[] = []; // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } if (!folderId) { folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); @@ -575,11 +570,11 @@ export const getSecretsHelper = async ({ const postHogClient = await TelemetryService.getPostHogClient(); // reduce the number of events captured - let shouldRecordK8Event = false + let shouldRecordK8Event = false; if (authData.userAgent == K8_USER_AGENT_NAME) { const randomNumber = Math.random(); if (randomNumber > 0.9) { - shouldRecordK8Event = true + shouldRecordK8Event = true; } } @@ -588,7 +583,7 @@ export const getSecretsHelper = async ({ if (postHogClient && atLeastOneNonSignUpSecret) { const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; - const approximateForNoneCapturedEvents = secrets.length * 10 + const approximateForNoneCapturedEvents = secrets.length * 10; if (shouldCapture) { postHogClient.capture({ @@ -633,11 +628,7 @@ export const getSecretHelper = async ({ }); let secret: ISecret | null = null; // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); // try getting personal secret first (if exists) @@ -751,12 +742,6 @@ export const updateSecretHelper = async ({ }); let secret: ISecret | null = null; - // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); if (type === SECRET_SHARED) { @@ -916,12 +901,6 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId) }); - // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); let secrets: ISecret[] = []; diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 59030ff8c..728c7d089 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,7 +1,4 @@ -import { - IUser, - User, -} from "../models"; +import { IUser, User } from "../models"; import { sendMail } from "./nodemailer"; /** @@ -12,10 +9,10 @@ import { sendMail } from "./nodemailer"; */ export const setupAccount = async ({ email }: { email: string }) => { const user = await new User({ - email, + email }).save(); - return user; + return user; }; /** @@ -37,36 +34,36 @@ export const setupAccount = async ({ email }: { email: string }) => { * @returns {Object} user - the completed user */ export const completeAccount = async ({ - userId, - firstName, - lastName, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, + userId, + firstName, + lastName, + encryptionVersion, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier }: { - userId: string; - firstName: string; - lastName: string; - encryptionVersion: number; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; + userId: string; + firstName: string; + lastName?: string; + encryptionVersion: number; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; }) => { const options = { - new: true, + new: true }; const user = await User.findByIdAndUpdate( userId, @@ -82,12 +79,12 @@ export const completeAccount = async ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, salt, - verifier, + verifier }, options ); - return user; + return user; }; /** @@ -98,38 +95,42 @@ export const completeAccount = async ({ * @param {String} obj.userAgent - login user-agent */ export const checkUserDevice = async ({ - user, - ip, - userAgent, + user, + ip, + userAgent }: { - user: IUser; - ip: string; - userAgent: string; + user: IUser; + ip: string; + userAgent: string; }) => { - const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent); - - if (!isDeviceSeen) { - // case: unseen login ip detected for user - // -> notify user about the sign-in from new ip - - user.devices = user.devices.concat([{ - ip: String(ip), - userAgent, - }]); - - await user.save(); + const isDeviceSeen = user.devices.some( + (device) => device.ip === ip && device.userAgent === userAgent + ); - // send MFA code [code] to [email] - await sendMail({ - template: "newDevice.handlebars", - subjectLine: "Successful login from new device", - recipients: [user.email], - substitutions: { - email: user.email, - timestamp: new Date().toString(), - ip, - userAgent, - }, - }); - } -} \ No newline at end of file + if (!isDeviceSeen) { + // case: unseen login ip detected for user + // -> notify user about the sign-in from new ip + + user.devices = user.devices.concat([ + { + ip: String(ip), + userAgent + } + ]); + + await user.save(); + + // send MFA code [code] to [email] + await sendMail({ + template: "newDevice.handlebars", + subjectLine: "Successful login from new device", + recipients: [user.email], + substitutions: { + email: user.email, + timestamp: new Date().toString(), + ip, + userAgent + } + }); + } +}; diff --git a/backend/src/helpers/validation.ts b/backend/src/helpers/validation.ts new file mode 100644 index 000000000..f552eb69b --- /dev/null +++ b/backend/src/helpers/validation.ts @@ -0,0 +1,17 @@ +import type { Request } from "express"; +import { AnyZodObject, ZodError, z } from "zod"; +import { BadRequestError } from "../utils/errors"; + +export async function validateRequest( + schema: T, + req: Request +): Promise> { + try { + return schema.parseAsync(req); + } catch (error) { + if (error instanceof ZodError) { + throw BadRequestError({ message: error.message }); + } + return BadRequestError({ message: JSON.stringify(error) }); + } +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 098da2aad..7a5723b04 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -37,6 +37,7 @@ import { membership as v1MembershipRouter, organization as v1OrganizationRouter, password as v1PasswordRouter, + role as v1RoleRouter, secretImport as v1SecretImportRouter, secret as v1SecretRouter, secretsFolder as v1SecretsFolder, @@ -173,6 +174,7 @@ const main = async () => { app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/secret-imports", v1SecretImportRouter); + app.use("/api/v1/roles", v1RoleRouter); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index bc2cbc969..b8c88bd26 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -14,6 +14,9 @@ import requireServiceAccountAuth from "./requireServiceAccountAuth"; import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth"; import requireSecretAuth from "./requireSecretAuth"; import requireSecretsAuth from "./requireSecretsAuth"; +import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled"; +import requireE2EEOff from "./requireE2EEOff"; +import requireIPAllowlistCheck from "./requireIPAllowlistCheck"; import validateRequest from "./validateRequest"; export { @@ -33,5 +36,8 @@ export { requireServiceAccountWorkspacePermissionAuth, requireSecretAuth, requireSecretsAuth, + requireBlindIndicesEnabled, + requireE2EEOff, + requireIPAllowlistCheck, validateRequest, }; diff --git a/backend/src/middleware/requireBlindIndicesEnabled.ts b/backend/src/middleware/requireBlindIndicesEnabled.ts new file mode 100644 index 000000000..b1288dd2b --- /dev/null +++ b/backend/src/middleware/requireBlindIndicesEnabled.ts @@ -0,0 +1,34 @@ +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { SecretBlindIndexData } from "../models"; +import { UnauthorizedRequestError } from "../utils/errors"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has blind indices enabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireBlindIndicesEnabled = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + const secretBlindIndexData = await SecretBlindIndexData.exists({ + workspace: new Types.ObjectId(workspaceId) + }); + + if (!secretBlindIndexData) throw UnauthorizedRequestError({ + message: "Failed workspace authorization due to blind indices not being enabled" + }); + + return next(); + } +} + +export default requireBlindIndicesEnabled; \ No newline at end of file diff --git a/backend/src/middleware/requireE2EEOff.ts b/backend/src/middleware/requireE2EEOff.ts new file mode 100644 index 000000000..a9e5a735b --- /dev/null +++ b/backend/src/middleware/requireE2EEOff.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from "express"; +import { BadRequestError } from "../utils/errors"; +import { BotService } from "../services"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has E2EE off/disabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireE2EEOff = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, _: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); + + if (isWorkspaceE2EE) throw BadRequestError({ + message: "Failed workspace authorization due to end-to-end encryption not being disabled" + }); + + return next(); + } +} + +export default requireE2EEOff; \ No newline at end of file diff --git a/backend/src/middleware/requireIPAllowlistCheck.ts b/backend/src/middleware/requireIPAllowlistCheck.ts new file mode 100644 index 000000000..411c802a9 --- /dev/null +++ b/backend/src/middleware/requireIPAllowlistCheck.ts @@ -0,0 +1,55 @@ +import net from "net"; +import { NextFunction, Request, Response } from "express"; +import { UnauthorizedRequestError } from "../utils/errors"; +import { extractIPDetails } from "../utils/ip"; +import { ActorType, TrustedIP } from "../ee/models"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has E2EE off/disabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireIPAllowlistCheck = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, _: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + if (req.authData.actor.type === ActorType.SERVICE) { + const trustedIps = await TrustedIP.find({ + workspace: workspaceId + }); + + if (trustedIps.length > 0) { + // case: check the IP address of the inbound request against trusted IPs + + const blockList = new net.BlockList(); + + for (const trustedIp of trustedIps) { + if (trustedIp.prefix !== undefined) { + blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type); + } else { + blockList.addAddress(trustedIp.ipAddress, trustedIp.type); + } + } + + const { type } = extractIPDetails(req.authData.ipAddress); + const check = blockList.check(req.authData.ipAddress, type); + + if (!check) + throw UnauthorizedRequestError({ + message: "Failed workspace authorization" + }); + } + } + + return next(); + } +} + +export default requireIPAllowlistCheck; \ No newline at end of file diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index f6f7405a9..bbf829565 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -9,24 +9,18 @@ type req = "params" | "body" | "query"; * on request params. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth - * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireWorkspaceAuth = ({ acceptedRoles, locationWorkspaceId, locationEnvironment = undefined, requiredPermissions = [], - requireBlindIndicesEnabled = false, - requireE2EEOff = false, - checkIPAllowlist = false }: { acceptedRoles: Array<"admin" | "member">; locationWorkspaceId: req; locationEnvironment?: req | undefined; requiredPermissions?: string[]; - requireBlindIndicesEnabled?: boolean; - requireE2EEOff?: boolean; - checkIPAllowlist?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { const workspaceId = req[locationWorkspaceId]?.workspaceId; @@ -38,10 +32,7 @@ const requireWorkspaceAuth = ({ workspaceId: new Types.ObjectId(workspaceId), environment, acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled, - requireE2EEOff, - checkIPAllowlist + requiredPermissions }); if (membership) { diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index 6c32ff64f..22a3819e2 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -1,55 +1,60 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, MEMBER } from "../variables"; +import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; export interface IMembershipPermission { - environmentSlug: string, - ability: string + environmentSlug: string; + ability: string; } export interface IMembership { - _id: Types.ObjectId; - user: Types.ObjectId; - inviteEmail?: string; - workspace: Types.ObjectId; - role: "admin" | "member"; - deniedPermissions: IMembershipPermission[] + _id: Types.ObjectId; + user: Types.ObjectId; + inviteEmail?: string; + workspace: Types.ObjectId; + role: "admin" | "member" | "viewer" | "custom"; + customRole: Types.ObjectId; + deniedPermissions: IMembershipPermission[]; } const membershipSchema = new Schema( - { - user: { - type: Schema.Types.ObjectId, - ref: "User", - }, - inviteEmail: { - type: String, - }, - workspace: { - type: Schema.Types.ObjectId, - ref: "Workspace", - required: true, - }, - deniedPermissions: { - type: [ - { - environmentSlug: String, - ability: { - type: String, - enum: ["read", "write"], - }, - }, - ], - default: [], - }, - role: { - type: String, - enum: [ADMIN, MEMBER], - required: true, - }, - }, - { - timestamps: true, - } + { + user: { + type: Schema.Types.ObjectId, + ref: "User" + }, + inviteEmail: { + type: String + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true + }, + deniedPermissions: { + type: [ + { + environmentSlug: String, + ability: { + type: String, + enum: ["read", "write"] + } + } + ], + default: [] + }, + role: { + type: String, + enum: [ADMIN, MEMBER, VIEWER, CUSTOM], + required: true + }, + customRole: { + type: Schema.Types.ObjectId, + ref: "Role" + } + }, + { + timestamps: true + } ); export const Membership = model("Membership", membershipSchema); \ No newline at end of file diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index b45f9cfe8..530797379 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,45 +1,47 @@ import { Document, Schema, Types, model } from "mongoose"; -import { ACCEPTED, ADMIN, INVITED, MEMBER, OWNER } from "../variables"; +import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, OWNER } from "../variables"; export interface IMembershipOrg extends Document { - _id: Types.ObjectId; - user: Types.ObjectId; - inviteEmail: string; - organization: Types.ObjectId; - role: "owner" | "admin" | "member"; - status: "invited" | "accepted"; + _id: Types.ObjectId; + user: Types.ObjectId; + inviteEmail: string; + organization: Types.ObjectId; + role: "owner" | "admin" | "member" | "custom"; + customRole: Types.ObjectId; + status: "invited" | "accepted"; } const membershipOrgSchema = new Schema( - { - user: { - type: Schema.Types.ObjectId, - ref: "User", - }, - inviteEmail: { - type: String, - }, - organization: { - type: Schema.Types.ObjectId, - ref: "Organization", - }, - role: { - type: String, - enum: [OWNER, ADMIN, MEMBER], - required: true, - }, - status: { - type: String, - enum: [INVITED, ACCEPTED], - required: true, - }, - }, - { - timestamps: true, - } + { + user: { + type: Schema.Types.ObjectId, + ref: "User" + }, + inviteEmail: { + type: String + }, + organization: { + type: Schema.Types.ObjectId, + ref: "Organization" + }, + role: { + type: String, + enum: [OWNER, ADMIN, MEMBER, CUSTOM], + required: true + }, + status: { + type: String, + enum: [INVITED, ACCEPTED], + required: true + }, + customRole: { + type: Schema.Types.ObjectId, + ref: "Role" + } + }, + { + timestamps: true + } ); -export const MembershipOrg = model( - "MembershipOrg", - membershipOrgSchema -); \ No newline at end of file +export const MembershipOrg = model("MembershipOrg", membershipOrgSchema); diff --git a/backend/src/models/role.ts b/backend/src/models/role.ts new file mode 100644 index 000000000..643acedcd --- /dev/null +++ b/backend/src/models/role.ts @@ -0,0 +1,55 @@ +import { Schema, Types, model } from "mongoose"; + +export interface IRole { + _id: Types.ObjectId; + name: string; + description: string; + slug: string; + permissions: Array; + workspace: Types.ObjectId; + organization: Types.ObjectId; + isOrgRole: boolean; +} + +const roleSchema = new Schema( + { + name: { + type: String, + required: true + }, + organization: { + type: Schema.Types.ObjectId, + ref: "Organization", + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace" + }, + isOrgRole: { + type: Boolean, + required: true, + select: false + }, + description: { + type: String + }, + slug: { + type: String, + required: true + }, + permissions: { + type: Array, + required: true + } + }, + { + timestamps: true + } +); + +roleSchema.index({ organization: 1, workspace: 1 }); + +const Role = model("Role", roleSchema); + +export default Role; diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index d9c0d6120..a3037f311 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 ); 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/index.ts b/backend/src/routes/v1/index.ts index 08298a1c6..5edab6a18 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth"; import secretsFolder from "./secretsFolder"; import webhooks from "./webhook"; import secretImport from "./secretImport"; +import role from "./role"; export { signup, @@ -37,5 +38,6 @@ export { integrationAuth, secretsFolder, webhooks, - secretImport + secretImport, + role }; diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 22aa901ea..6dda7527b 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -1,45 +1,14 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireIntegrationAuth, - requireIntegrationAuthorizationAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; -import { body, param } from "express-validator"; +import { requireAuth } from "../../middleware"; +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 +17,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 +25,6 @@ router.delete( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationId").exists().trim(), - validateRequest, integrationController.deleteIntegration ); @@ -81,13 +33,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..cfb27ee71 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -1,202 +1,111 @@ 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( - "/:integrationAuthId/teamcity/build-configs", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthTeamCityBuildConfigs + "/:integrationAuthId/teamcity/build-configs", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + 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/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index f79ce61d1..0fa4ffbfe 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -8,23 +8,20 @@ import { AuthMode } from "../../variables"; // TODO endpoint: consider moving these endpoints to be under /organization to be more RESTful router.post( - "/signup", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("inviteeEmail").exists().trim().notEmpty().isEmail(), - body("organizationId").exists().trim().notEmpty(), - validateRequest, - membershipOrgController.inviteUserToOrganization + "/signup", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipOrgController.inviteUserToOrganization ); router.post( - "/verify", - body("email").exists().trim().notEmpty(), - body("organizationId").exists().trim().notEmpty(), - body("code").exists().trim().notEmpty(), - validateRequest, - membershipOrgController.verifyUserToOrganization + "/verify", + body("email").exists().trim().notEmpty(), + body("organizationId").exists().trim().notEmpty(), + body("code").exists().trim().notEmpty(), + validateRequest, + membershipOrgController.verifyUserToOrganization ); 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 cf38c7cbc..54b3b7c9e 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -1,53 +1,37 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; -import { requireAuth, validateRequest } from "../../middleware"; +import { requireAuth } 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) // TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful -router.get( // TODO endpoint: deprecate - used for old CLI (deprecate) - "/:workspaceId/connect", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("workspaceId").exists().trim(), - validateRequest, - membershipController.validateMembership +router.get( + // TODO endpoint: deprecate - used for old CLI (deprecate) + "/:workspaceId/connect", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.validateMembership ); -router.delete( // TODO endpoint: check dashboard - "/:membershipId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipId").exists().trim(), - validateRequest, - membershipController.deleteMembership +router.delete( + // TODO endpoint: check dashboard + "/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.deleteMembership ); -router.post( // TODO endpoint: check dashboard - "/:membershipId/change-role", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("role").exists().trim(), - validateRequest, - 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 +router.post( + // TODO endpoint: check dashboard + "/:membershipId/change-role", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.changeMembershipRole ); export default router; diff --git a/backend/src/routes/v1/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts index 34899072b..1c7c47f88 100644 --- a/backend/src/routes/v1/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -5,24 +5,23 @@ import { requireAuth, validateRequest } from "../../middleware"; import { membershipOrgController } from "../../controllers/v1"; import { AuthMode } from "../../variables"; -router.post( // TODO endpoint: check dashboard - "/membershipOrg/:membershipOrgId/change-role", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipOrgId"), - validateRequest, - membershipOrgController.changeMembershipOrgRole +router.post( + // TODO endpoint: check dashboard + "/membershipOrg/:membershipOrgId/change-role", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + param("membershipOrgId"), + validateRequest, + membershipOrgController.changeMembershipOrgRole ); router.delete( - "/:membershipOrgId", // TODO endpoint: check dashboard - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipOrgId").exists().trim(), - validateRequest, - membershipOrgController.deleteMembershipOrg + "/:membershipOrgId", // TODO endpoint: check dashboard + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipOrgController.deleteMembershipOrg ); export default router; diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 78672093d..011d5fed6 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -1,166 +1,99 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; -import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../middleware"; -import { - ACCEPTED, - ADMIN, - AuthMode, - MEMBER, - OWNER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { organizationController } from "../../controllers/v1"; -router.get( // TODO endpoint: deprecate (moved to api/v2/users/me/organizations) - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - organizationController.getOrganizations +router.get( + // TODO endpoint: deprecate (moved to api/v2/users/me/organizations) + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizations ); -router.post( // not used on frontend - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("organizationName").exists().trim().notEmpty(), - validateRequest, - organizationController.createOrganization +router.post( + // not used on frontend + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.createOrganization ); router.get( - "/:organizationId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganization + "/:organizationId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganization ); -router.get( // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships) - "/:organizationId/users", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationMembers +router.get( + // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships) + "/:organizationId/users", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationMembers ); -router.get( // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces - "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationWorkspaces +router.get( + // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces + "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationWorkspaces ); router.patch( - "/:organizationId/name", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("name").exists().trim().notEmpty(), - validateRequest, - organizationController.changeOrganizationName + "/:organizationId/name", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.changeOrganizationName ); router.get( - "/:organizationId/incidentContactOrg", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationIncidentContacts + "/:organizationId/incidentContactOrg", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationIncidentContacts ); router.post( - "/:organizationId/incidentContactOrg", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("email").exists().trim().notEmpty(), - validateRequest, - organizationController.addOrganizationIncidentContact + "/:organizationId/incidentContactOrg", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.addOrganizationIncidentContact ); router.delete( - "/:organizationId/incidentContactOrg", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("email").exists().trim().notEmpty(), - validateRequest, - organizationController.deleteOrganizationIncidentContact + "/:organizationId/incidentContactOrg", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.deleteOrganizationIncidentContact ); router.post( - "/:organizationId/customer-portal-session", // TODO endpoint: move to EE - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.createOrganizationPortalSession + "/:organizationId/customer-portal-session", // TODO endpoint: move to EE + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.createOrganizationPortalSession ); router.get( - "/:organizationId/workspace-memberships", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationMembersAndTheirWorkspaces + "/:organizationId/workspace-memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationMembersAndTheirWorkspaces ); - 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/role.ts b/backend/src/routes/v1/role.ts new file mode 100644 index 000000000..042113954 --- /dev/null +++ b/backend/src/routes/v1/role.ts @@ -0,0 +1,33 @@ +import express from "express"; +import { roleController } from "../../controllers/v1"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; + +const router = express.Router(); + +router.post("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.createRole); + +router.patch("/:id", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.updateRole); + +router.delete( + "/:id", + requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), + roleController.deleteRole +); + +router.get("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.getRoles); + +// get a user permissions in an org +router.get( + "/organization/:orgId/permissions", + requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), + roleController.getUserPermissions +); + +router.get( + "/workspace/:workspaceId/permissions", + requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), + roleController.getUserWorkspacePermissions +); + +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..4dbc121b9 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 } from "../../middleware"; +import { AuthMode } 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 + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.createWorkspace ); router.delete( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.deleteWorkspace + "/:workspaceId", + 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] + }), + 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..45ea51594 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -1,96 +1,47 @@ 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 ); router.patch( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().isString().trim(), - body("environmentName").exists().isString().trim(), - body("otherEnvironmentSlug").exists().isString().trim(), - body("otherEnvironmentName").exists().isString().trim(), - validateRequest, environmentController.reorderWorkspaceEnvironments ); 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/organizations.ts b/backend/src/routes/v2/organizations.ts index 55f8606df..a12288886 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -1,102 +1,57 @@ import express from "express"; const router = express.Router(); import { - requireAuth, - requireMembershipOrgAuth, - requireOrganizationAuth, - validateRequest, + requireAuth, + requireOrganizationAuth } from "../../middleware"; -import { body, param } from "express-validator"; -import { - ACCEPTED, - ADMIN, - AuthMode, - MEMBER, - OWNER -} from "../../variables"; +import { ACCEPTED, ADMIN, AuthMode, OWNER } from "../../variables"; import { organizationsController } from "../../controllers/v2"; // TODO: /POST to create membership router.get( - "/:organizationId/memberships", - param("organizationId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.getOrganizationMemberships + "/:organizationId/memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.getOrganizationMemberships ); router.patch( - "/:organizationId/memberships/:membershipId", - param("organizationId").exists().trim(), - param("membershipId").exists().trim(), - body("role").exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - requireMembershipOrgAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.updateOrganizationMembership + "/:organizationId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.updateOrganizationMembership ); router.delete( - "/:organizationId/memberships/:membershipId", - param("organizationId").exists().trim(), - param("membershipId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - requireMembershipOrgAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.deleteOrganizationMembership + "/:organizationId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.deleteOrganizationMembership ); router.get( - "/:organizationId/workspaces", - param("organizationId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.getOrganizationWorkspaces + "/:organizationId/workspaces", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.getOrganizationWorkspaces ); -router.get( // TODO endpoint: deprecate service accounts - "/:organizationId/service-accounts", - param("organizationId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.getOrganizationServiceAccounts +router.get( + // TODO endpoint: deprecate service accounts + "/:organizationId/service-accounts", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + organizationsController.getOrganizationServiceAccounts ); -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..c175335ff 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,44 +17,18 @@ 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] }), - requireWorkspaceAuth({ - 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 +89,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 +104,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 +140,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..1765c2e33 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,288 +1,130 @@ import express from "express"; const router = express.Router(); -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { body, param, query } from "express-validator"; +import { + requireAuth, + requireBlindIndicesEnabled, + requireE2EEOff +} from "../../middleware"; import { secretsController } from "../../controllers/v3"; import { - ADMIN, - AuthMode, - MEMBER, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, - SECRET_PERSONAL, - SECRET_SHARED + AuthMode } from "../../variables"; 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 ); 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], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" + }), + requireE2EEOff({ + locationWorkspaceId: "query" }), secretsController.getSecretByNameRaw ); 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], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.createSecretRaw ); 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], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.updateSecretByNameRaw ); 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], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.deleteSecretByNameRaw ); router.get( "/", - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("folderId").optional().isString().trim(), - 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], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" }), secretsController.getSecrets ); 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], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.createSecret ); 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], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" }), secretsController.getSecretByName ); 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], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.updateSecretByName ); router.delete( "/: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], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.deleteSecretByName ); 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 new file mode 100644 index 000000000..10dbd2302 --- /dev/null +++ b/backend/src/services/ProjectRoleService.ts @@ -0,0 +1,258 @@ +import { + AbilityBuilder, + ForcedSubject, + MongoAbility, + RawRuleOf, + buildMongoQueryMatcher, + createMongoAbility +} from "@casl/ability"; +import { Membership } from "../models"; +import { IRole } from "../models/role"; +import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; +import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; +import picomatch from "picomatch"; + +const $glob: FieldInstruction = { + type: "field", + validate(instruction, value) { + if (typeof value !== "string") { + throw new Error(`"${instruction.name}" expects value to be a string`); + } + } +}; + +const glob: JsInterpreter> = (node, object, context) => { + const secretPath = context.get(object, node.field); + const permissionSecretGlobPath = node.value; + return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }); +}; + +export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); + +export enum ProjectPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum ProjectPermissionSub { + Role = "role", + Member = "member", + Settings = "settings", + Integrations = "integrations", + Webhooks = "webhooks", + ServiceTokens = "service-tokens", + Environments = "environments", + Tags = "tags", + AuditLogs = "audit-logs", + IpAllowList = "ip-allowlist", + Workspace = "workspace", + Secrets = "secrets", + SecretImports = "secret-imports", + SecretRollback = "secret-rollback", + Folders = "folders" +} + +type SubjectFields = { + environment: string; + secretPath: string; +}; + +export type ProjectPermissionSet = + | [ + ProjectPermissionActions, + ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) + ] + | [ + ProjectPermissionActions, + ProjectPermissionSub.Folders | (ForcedSubject & SubjectFields) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SubjectFields) + ) + ] + | [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(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Folders); + + 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(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs); + + 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.Workspace); + + return build({ conditionsMatcher }); +}; + +export const adminProjectPermissions = buildAdminPermission(); + +const buildMemberPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Folders); + + 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(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({ conditionsMatcher }); +}; + +export const memberProjectPermissions = buildMemberPermission(); + +const buildViewerPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + 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({ conditionsMatcher }); +}; + +export const viewerProjectPermission = buildViewerPermission(); + +export const getUserProjectPermissions = async (userId: string, workspaceId: string) => { + // TODO(akhilmhdh): speed this up by pulling from cache later + const membership = await Membership.findOne({ + user: userId, + workspace: workspaceId + }) + .populate<{ + customRole: IRole & { permissions: RawRuleOf>[] }; + }>("customRole") + .exec(); + + if (!membership || (membership.role === "custom" && !membership.customRole)) { + throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); + } + + if (membership.role === "admin") return { permission: adminProjectPermissions, membership }; + if (membership.role === "member") return { permission: memberProjectPermissions, membership }; + if (membership.role === "viewer") return { permission: viewerProjectPermission, membership }; + + if (membership.role === "custom") { + const permission = createMongoAbility(membership.customRole.permissions, { + conditionsMatcher + }); + return { permission, membership }; + } + + throw BadRequestError({ message: "User role not found" }); +}; diff --git a/backend/src/services/RoleService.ts b/backend/src/services/RoleService.ts new file mode 100644 index 000000000..bc5ef7453 --- /dev/null +++ b/backend/src/services/RoleService.ts @@ -0,0 +1,130 @@ +import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; +import { MembershipOrg } from "../models"; +import { IRole } from "../models/role"; +import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; +import { ACCEPTED } from "../variables"; +import { conditionsMatcher } from "./ProjectRoleService"; + +export enum OrgPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum OrgPermissionSubjects { + Workspace = "workspace", + Role = "role", + Member = "member", + Settings = "settings", + IncidentAccount = "incident-contact", + Sso = "sso", + Billing = "billing", + SecretScanning = "secret-scanning" +} + +export type OrgPermissionSet = + | [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionActions, OrgPermissionSubjects.Member] + | [OrgPermissionActions, OrgPermissionSubjects.Settings] + | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] + | [OrgPermissionActions, OrgPermissionSubjects.Sso] + | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] + | [OrgPermissionActions, OrgPermissionSubjects.Billing]; + +const buildAdminPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + // ws permissions + can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + // role permission + can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); + + return build({ conditionsMatcher }); +}; + +export const adminPermissions = buildAdminPermission(); + +const buildMemberPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + + return build({ conditionsMatcher }); +}; + +export const memberPermissions = buildMemberPermission(); + +export const getUserOrgPermissions = async (userId: string, orgId: string) => { + // TODO(akhilmhdh): speed this up by pulling from cache later + const membership = await MembershipOrg.findOne({ + user: userId, + organization: orgId, + status: ACCEPTED + }) + .populate<{ customRole: IRole & { permissions: RawRuleOf>[] } }>( + "customRole" + ) + .exec(); + + if (!membership || (membership.role === "custom" && !membership.customRole)) { + throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); + } + + if (membership.role === "admin" || membership.role === "owner") + return { permission: adminPermissions, membership }; + + if (membership.role === "member") return { permission: memberPermissions, membership }; + + if (membership.role === "custom") { + const permission = createMongoAbility(membership.customRole.permissions, { + conditionsMatcher + }); + return { permission, membership }; + } + + throw BadRequestError({ message: "User role not found" }); +}; diff --git a/backend/src/types/secret/index.d.ts b/backend/src/types/secret/index.d.ts index f60fdfd4c..05016b38e 100644 --- a/backend/src/types/secret/index.d.ts +++ b/backend/src/types/secret/index.d.ts @@ -28,7 +28,13 @@ export interface BatchSecretRequest { } export interface BatchSecret { - _id: string; + version?: number; + _id?: string; + user?: string; + environment: string; + workspace?: string; + algorithm?: string; + keyEncoding?: string; type: "shared" | "personal"; secretName: string; secretBlindIndex: string; @@ -42,5 +48,5 @@ export interface BatchSecret { secretCommentIV: string; secretCommentTag: string; tags: string[]; - folder: string + folder: string; } diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 9c301722a..ae6279bfc 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -3,13 +3,7 @@ import crypto from "crypto"; import { Types } from "mongoose"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { EESecretService } from "../../ee/services"; -import { - IPType, - ISecretVersion, - SecretSnapshot, - SecretVersion, - TrustedIP -} from "../../ee/models"; +import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models"; import { AuthMethod, BackupPrivateKey, @@ -18,6 +12,7 @@ import { ISecret, Integration, IntegrationAuth, + Membership, Organization, Secret, SecretBlindIndexData, @@ -30,7 +25,9 @@ import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8 + ENCODING_SCHEME_UTF8, + MEMBER, + VIEWER } from "../../variables"; import { InternalServerError } from "../errors"; @@ -582,7 +579,7 @@ export const backfillTrustedIps = async () => { filter: { workspace: Types.ObjectId; ipAddress: string; - }, + }; update: { workspace: Types.ObjectId; ipAddress: string; @@ -590,9 +587,9 @@ export const backfillTrustedIps = async () => { prefix: number; isActive: boolean; comment: string; - }, + }; upsert: boolean; - } + }; }[] = []; workspaceIdsToAddTrustedIp.forEach((workspaceId) => { @@ -638,7 +635,7 @@ export const backfillTrustedIps = async () => { await TrustedIP.bulkWrite(operations); console.log("Backfill: Trusted IPs complete"); } -} +}; export const backfillUserAuthMethods = async () => { await User.updateMany( @@ -655,7 +652,6 @@ export const backfillUserAuthMethods = async () => { } ); - const documentsToUpdate = await User.find({ authProvider: { $exists: true }, authMethods: { $exists: false } @@ -676,4 +672,24 @@ export const backfillUserAuthMethods = async () => { } ); } -} +}; + +export const backfillPermission = async () => { + await Membership.updateMany( + { + deniedPermissions: { + $exists: true, + $ne: [] + }, + role: MEMBER + }, + [ + { + $set: { + role: VIEWER + } + } + ] + ); + console.log("Backfill: Finishing converting old denied permission in workspace to viewers"); +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 90072eb6f..7ebdd2563 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -11,6 +11,7 @@ import { backfillBots, backfillEncryptionMetadata, backfillIntegration, + backfillPermission, backfillSecretBlindIndexData, backfillSecretFolders, backfillSecretVersions, @@ -24,12 +25,7 @@ import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData"; -import { - getMongoURL, - getNodeEnv, - getRedisUrl, - getSentryDSN -} from "../../config"; +import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config"; import { initializePassport } from "../auth"; /** @@ -43,8 +39,10 @@ import { initializePassport } from "../auth"; * - Re-encrypting data */ export const setup = async () => { - if (await getRedisUrl() === undefined || await getRedisUrl() === "") { - console.error("WARNING: Redis is not yet configured. Infisical may not function as expected without it.") + if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") { + console.error( + "WARNING: Redis is not yet configured. Infisical may not function as expected without it." + ); } await validateEncryptionKeysConfig(); @@ -86,6 +84,7 @@ export const setup = async () => { await backfillServiceTokenMultiScope(); await backfillTrustedIps(); await backfillUserAuthMethods(); + await backfillPermission(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY 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..f32fc1197 --- /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().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(), + 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..6cf7cf68a --- /dev/null +++ b/backend/src/validation/environments.ts @@ -0,0 +1,49 @@ +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() + }) +}); + +export const ReorderWorkspaceEnvironmentsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim(), + environmentName: z.string().trim(), + otherEnvironmentSlug: z.string().trim(), + otherEnvironmentName: 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/index.ts b/backend/src/validation/index.ts index 4cc25450f..c3ed7696c 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -1,4 +1,5 @@ export * from "./user"; +export * from "./role"; export * from "./workspace"; export * from "./bot"; export * from "./integration"; @@ -8,4 +9,4 @@ export * from "./membershipOrg"; export * from "./organization"; export * from "./secrets"; export * from "./serviceAccount"; -export * from "./serviceTokenData"; \ No newline at end of file +export * from "./serviceTokenData"; diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index b1143a9f7..db8b0ee8b 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,87 @@ 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() + }) +}); + +export const ManualSyncV1 = z.object({ + body: z.object({ + environment: z.string().trim(), + workspaceId: 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..233805545 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -1,16 +1,11 @@ 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"; /** * Validate authenticated clients for membership with id [membershipId] based @@ -22,36 +17,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.string().trim() + }), + 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/membershipOrg.ts b/backend/src/validation/membershipOrg.ts index b0ada6a61..02f9cac08 100644 --- a/backend/src/validation/membershipOrg.ts +++ b/backend/src/validation/membershipOrg.ts @@ -1,16 +1,10 @@ import { Types } from "mongoose"; -import { - MembershipOrg, -} from "../models"; -import { - validateMembershipOrg, -} from "../helpers/membershipOrg"; -import { - MembershipOrgNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { MembershipOrg } from "../models"; +import { validateMembershipOrg } from "../helpers/membershipOrg"; +import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for organization membership with id [membershipOrgId] based @@ -22,35 +16,57 @@ import { ActorType } from "../ee/models"; * @param {MembershipOrg} - validated organization membership */ export const validateClientForMembershipOrg = async ({ - authData, - membershipOrgId, - acceptedRoles, - acceptedStatuses, + authData, + membershipOrgId, + acceptedRoles, + acceptedStatuses }: { - authData: AuthData; - membershipOrgId: Types.ObjectId; - acceptedRoles: Array<"owner" | "admin" | "member">; - acceptedStatuses: Array<"invited" | "accepted">; + authData: AuthData; + membershipOrgId: Types.ObjectId; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { - const membershipOrg = await MembershipOrg.findById(membershipOrgId); + const membershipOrg = await MembershipOrg.findById(membershipOrgId); - if (!membershipOrg) throw MembershipOrgNotFoundError({ - message: "Failed to find organization membership ", - }); - - switch (authData.actor.type) { - case ActorType.USER: - await validateMembershipOrg({ - userId: authData.authPayload._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses, - }); - - return membershipOrg; - case ActorType.SERVICE: - throw UnauthorizedRequestError({ - message: "Failed service account client authorization for organization membership", - }); - } -} \ No newline at end of file + if (!membershipOrg) + throw MembershipOrgNotFoundError({ + message: "Failed to find organization membership " + }); + + switch (authData.actor.type) { + case ActorType.USER: + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service account client authorization for organization membership" + }); + } +}; + +export const DelOrgMembershipv1 = z.object({ + params: z.object({ + membershipOrgId: z.string().trim() + }) +}); + +export const InviteUserToOrgv1 = z.object({ + body: z.object({ + inviteeEmail: z.string().trim().email(), + organizationId: z.string().trim() + }) +}); + +export const VerifyUserToOrgv1 = z.object({ + body: z.object({ + email: z.string().trim().email(), + organizationId: z.string().trim(), + code: z.string().trim() + }) +}); diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index 4ca9811c5..9aa13cbb6 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -1,12 +1,7 @@ import { Types } from "mongoose"; -import { - IUser, - Organization, -} from "../models"; -import { - OrganizationNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { z } from "zod"; +import { IUser, Organization } from "../models"; +import { OrganizationNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { validateUserClientForOrganization } from "./user"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; @@ -21,7 +16,7 @@ export const validateClientForOrganization = async ({ authData, organizationId, acceptedRoles, - acceptedStatuses, + acceptedStatuses }: { authData: AuthData; organizationId: Types.ObjectId; @@ -32,10 +27,10 @@ export const validateClientForOrganization = async ({ if (!organization) { throw OrganizationNotFoundError({ - message: "Failed to find organization", + message: "Failed to find organization" }); } - + let membershipOrg; switch (authData.actor.type) { case ActorType.USER: @@ -43,13 +38,170 @@ export const validateClientForOrganization = async ({ user: authData.authPayload as IUser, organization, acceptedRoles, - acceptedStatuses, + acceptedStatuses }); - return { organization, membershipOrg }; + return { organization, membershipOrg }; case ActorType.SERVICE: throw UnauthorizedRequestError({ - message: "Failed service token authorization for organization", + message: "Failed service token authorization for organization" }); } -}; \ No newline at end of file +}; + +export const GetOrgPlansTablev1 = z.object({ + query: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }), + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgPlanv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + query: z.object({ workspaceId: z.string().trim().optional() }) +}); + +export const StartOrgTrailv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ success_url: z.string().trim() }) +}); + +export const GetOrgPlanBillingInfov1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + query: z.object({ workspaceId: z.string().trim().optional() }) +}); + +export const GetOrgPlanTablev1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + query: z.object({ workspaceId: z.string().trim().optional() }) +}); + +export const GetOrgBillingDetailsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const UpdateOrgBillingDetailsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ + email: z.string().trim().email().optional(), + name: z.string().trim().optional() + }) +}); + +export const GetOrgPmtMethodsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgPmtMethodv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ + success_url: z.string().trim(), + cancel_url: z.string().trim() + }) +}); + +export const DelOrgPmtMethodv1 = z.object({ + params: z.object({ + organizationId: z.string().trim(), + pmtMethodId: z.string().trim() + }) +}); + +export const GetOrgTaxIdsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgTaxId = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ + type: z.string().trim(), + value: z.string().trim() + }) +}); + +export const DelOrgTaxIdv1 = z.object({ + params: z.object({ + organizationId: z.string().trim(), + taxId: z.string().trim() + }) +}); + +export const GetOrgInvoicesv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgLicencesv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgv1 = z.object({ + body: z.object({ + organizationName: z.string().trim() + }) +}); + +export const GetOrgv1 = z.object({ + params: z.object({ + organizationId: z.string().trim() + }) +}); + +export const GetOrgMembersv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgWorkspacesv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const ChangeOrgNamev1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ name: z.string().trim() }) +}); + +export const GetOrgIncidentContactv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgIncideContact = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ email: z.string().email().trim() }) +}); + +export const DelOrgIncideContact = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ email: z.string().email().trim() }) +}); + +export const CreateOrgPortalSessionv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgMembersAndWsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgMembersv2 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const UpdateOrgMemberv2 = z.object({ + params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }), + body: z.object({ + role: z.string().trim() + }) +}); + +export const DeleteOrgMemberv2 = z.object({ + params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }) +}); + +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/role.ts b/backend/src/validation/role.ts new file mode 100644 index 000000000..e3ecafe59 --- /dev/null +++ b/backend/src/validation/role.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +export const CreateRoleSchema = z.object({ + body: z.object({ + slug: z.string().trim(), + name: z.string().trim(), + description: z.string().trim().optional(), + workspaceId: z.string().trim().optional(), + orgId: z.string().trim(), + permissions: z + .object({ + subject: z.string().trim(), + action: z.string().trim(), + conditions: z + .record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })])) + .optional() + }) + .array() + }) +}); + +export const UpdateRoleSchema = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + slug: z.string().trim().optional(), + name: z.string().trim().optional(), + description: z.string().trim().optional(), + workspaceId: z.string().trim().optional(), + orgId: z.string().trim(), + permissions: z + .object({ + subject: z.string().trim(), + action: z.string().trim(), + conditions: z + .record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })])) + .optional() + }) + .array() + .optional() + }) +}); + +export const DeleteRoleSchema = z.object({ + params: z.object({ + id: z.string().trim() + }) +}); + +export const GetRoleSchema = z.object({ + query: z.object({ + workspaceId: z.string().trim().optional(), + orgId: z.string().trim() + }) +}); + +export const GetUserPermission = z.object({ + params: z.object({ + orgId: z.string().trim() + }) +}); + +export const GetUserProjectPermission = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); diff --git a/backend/src/validation/secretImports.ts b/backend/src/validation/secretImports.ts new file mode 100644 index 000000000..92867a67b --- /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().default("root") + }) +}); + +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/secretScanning.ts b/backend/src/validation/secretScanning.ts new file mode 100644 index 000000000..e16c67c5c --- /dev/null +++ b/backend/src/validation/secretScanning.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const CreateInstalLSessionv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const LinkInstallationToOrgv1 = z.object({ + body: z.object({ + installationId: z.number(), + sessionId: z.string().trim() + }) +}); + +export const GetOrgInstallStatusv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgRisksv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const UpdateRiskStatusv1 = z.object({ + params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }), + body: z.object({ status: z.string().trim() }) +}); 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..700a3ea5d 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,294 @@ 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.coerce.number(), + limit: z.coerce.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() + }) + .array() +}); + +export const BatchSecretsV2 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + folderId: z.string().trim().default("root"), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + 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("/"), + 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.record(z.string()).optional() + }), + params: z.object({ + secretName: z.string().trim() + }) +}); + +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..1379b8f97 100644 --- a/backend/src/validation/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -1,14 +1,11 @@ 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"; +import { isValidScope } from "../helpers"; /** * Validate authenticated clients for service token with id [serviceTokenId] based @@ -31,10 +28,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({ @@ -65,11 +63,13 @@ export const validateServiceTokenDataClientForWorkspace = async ({ serviceTokenData, workspaceId, environment, + secretPath = "/", requiredPermissions }: { serviceTokenData: IServiceTokenData; workspaceId: Types.ObjectId; environment?: string; + secretPath?: string; requiredPermissions?: string[]; }) => { if (!serviceTokenData.workspace.equals(workspaceId)) { @@ -81,7 +81,6 @@ export const validateServiceTokenDataClientForWorkspace = async ({ if (environment) { // case: environment is specified - if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === environment)) { // case: invalid environment passed throw UnauthorizedRequestError({ @@ -89,6 +88,10 @@ export const validateServiceTokenDataClientForWorkspace = async ({ }); } + if (!isValidScope(serviceTokenData, environment, secretPath)) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + requiredPermissions?.forEach((permission) => { if (!serviceTokenData.permissions.includes(permission)) { throw UnauthorizedRequestError({ @@ -140,3 +143,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/sso.ts b/backend/src/validation/sso.ts new file mode 100644 index 000000000..275ae611e --- /dev/null +++ b/backend/src/validation/sso.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { AuthProvider } from "../ee/models"; + +export const GetSsoConfigv1 = z.object({ + query: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateSsoConfigv1 = z.object({ + body: z.object({ + organizationId: z.string().trim(), + authProvider: z.nativeEnum(AuthProvider), + isActive: z.boolean(), + entryPoint: z.string().trim(), + issuer: z.string().trim(), + cert: z.string().trim() + }) +}); + +export const UpdateSsoConfigv1 = z.object({ + body: z.object({ + organizationId: z.string().trim(), + authProvider: z.nativeEnum(AuthProvider).optional(), + isActive: z.boolean().optional(), + entryPoint: z.string().trim().optional(), + issuer: z.string().trim().optional(), + cert: z.string().trim().optional() + }) +}); 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 3be8d0dad..44044d923 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -1,25 +1,12 @@ -import net from "net"; import { Types } from "mongoose"; -import { - IServiceTokenData, - IUser, - SecretBlindIndexData, - Workspace, -} from "../models"; -import { - ActorType, - TrustedIP -} from "../ee/models"; +import { IServiceTokenData, IUser, Workspace } from "../models"; +import { ActorType } from "../ee/models"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; -import { - BadRequestError, - UnauthorizedRequestError, - WorkspaceNotFoundError, -} from "../utils/errors"; -import { BotService } from "../services"; +import { WorkspaceNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; -import { extractIPDetails } from "../utils/ip"; +import { z } from "zod"; +import { EventType, UserAgentType } from "../ee/models"; /** * Validate authenticated clients for workspace with id [workspaceId] based @@ -32,106 +19,283 @@ import { extractIPDetails } from "../utils/ip"; * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForWorkspace = async ({ - authData, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled, - requireE2EEOff, - checkIPAllowlist + authData, + workspaceId, + environment, + acceptedRoles, + requiredPermissions }: { - authData: AuthData; - workspaceId: Types.ObjectId; - environment?: string; - acceptedRoles: Array<"admin" | "member">; - requiredPermissions?: string[]; - requireBlindIndicesEnabled: boolean; - requireE2EEOff: boolean; - checkIPAllowlist: boolean; + authData: AuthData; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<"admin" | "member">; + requiredPermissions?: string[]; }) => { - const workspace = await Workspace.findById(workspaceId); + const workspace = await Workspace.findById(workspaceId); - if (!workspace) throw WorkspaceNotFoundError({ - message: "Failed to find workspace", - }); + if (!workspace) throw WorkspaceNotFoundError({ + message: "Failed to find workspace" + }); - if (requireBlindIndicesEnabled) { - // case: blind indices are not enabled for secrets in this workspace - // (i.e. workspace was created before blind indices were introduced - // and no admin has enabled it) - - const secretBlindIndexData = await SecretBlindIndexData.exists({ - workspace: new Types.ObjectId(workspaceId), - }); - - if (!secretBlindIndexData) throw UnauthorizedRequestError({ - message: "Failed workspace authorization due to blind indices not being enabled", - }); - } - - if (requireE2EEOff) { - const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - - if (isWorkspaceE2EE) throw BadRequestError({ - message: "Failed workspace authorization due to end-to-end encryption not being disabled", - }); - } - - let membership; - switch (authData.actor.type) { - case ActorType.USER: - membership = await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - }); - - return ({ membership, workspace }); - case ActorType.SERVICE: - if (checkIPAllowlist) { - const trustedIps = await TrustedIP.find({ - workspace: workspaceId - }); - - if (trustedIps.length > 0) { - // case: check the IP address of the inbound request against trusted IPs + let membership; + switch (authData.actor.type) { + case ActorType.USER: + membership = await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId, + environment, + acceptedRoles, + requiredPermissions + }); - const blockList = new net.BlockList(); - - for (const trustedIp of trustedIps) { - if (trustedIp.prefix !== undefined) { - blockList.addSubnet( - trustedIp.ipAddress, - trustedIp.prefix, - trustedIp.type - ); - } else { - blockList.addAddress( - trustedIp.ipAddress, - trustedIp.type - ); - } - } - - const { type } = extractIPDetails(authData.ipAddress); - const check = blockList.check(authData.ipAddress, type); - - if (!check) throw UnauthorizedRequestError({ - message: "Failed workspace authorization" - }); - } - } + return { membership, workspace }; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId, + environment, + requiredPermissions + }); - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload as IServiceTokenData, - workspaceId, - environment, - requiredPermissions, - }); - - return {}; - } -} + return {}; + } +}; + +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.coerce.number(), + limit: z.coerce.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.coerce.number(), + limit: z.coerce.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.coerce.number(), + limit: z.coerce.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() + }) +}); diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 4f5620236..bccabc6e6 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -2,9 +2,11 @@ export const OWNER = "owner"; export const ADMIN = "admin"; export const MEMBER = "member"; +export const VIEWER = "viewer"; +export const CUSTOM = "custom"; // membership statuses export const INVITED = "invited"; // -- organization -export const ACCEPTED = "accepted"; \ No newline at end of file +export const ACCEPTED = "accepted"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 990b03377..e6847b42c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5,6 +5,8 @@ "packages": { "": { "dependencies": { + "@casl/ability": "^6.5.0", + "@casl/react": "^3.1.0", "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", @@ -19,7 +21,7 @@ "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^2.9.10", "@octokit/rest": "^19.0.7", - "@radix-ui/react-accordion": "^1.1.0", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -39,6 +41,7 @@ "@stripe/stripe-js": "^1.46.0", "@tanstack/react-query": "^4.23.0", "@types/argon2-browser": "^1.18.1", + "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -63,6 +66,7 @@ "markdown-it": "^13.0.1", "next": "^12.3.4", "nprogress": "^0.2.0", + "picomatch": "^2.3.1", "posthog-js": "^1.58.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -88,7 +92,8 @@ "uuid": "^8.3.2", "uuidv4": "^6.2.13", "yaml": "^2.2.2", - "yup": "^0.32.11" + "yup": "^0.32.11", + "zod": "^3.22.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.23", @@ -103,6 +108,7 @@ "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", + "@types/picomatch": "^2.3.0", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", @@ -2458,6 +2464,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, + "node_modules/@casl/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz", + "integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==", + "peerDependencies": { + "@casl/ability": "^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4235,24 +4261,230 @@ } }, "node_modules/@radix-ui/react-accordion": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz", - "integrity": "sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", "dependencies": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-collapsible": "1.0.2", - "@radix-ui/react-collection": "1.0.2", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@radix-ui/react-alert-dialog": { @@ -4307,23 +4539,210 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz", - "integrity": "sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", "dependencies": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@radix-ui/react-collection": { @@ -7975,6 +8394,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "node_modules/@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", @@ -8457,6 +8882,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -23188,6 +23644,14 @@ "engines": { "node": ">=10" } + }, + "node_modules/zod": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", + "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -24852,6 +25316,20 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "requires": { + "@ucast/mongo2js": "^1.3.0" + } + }, + "@casl/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz", + "integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==", + "requires": {} + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -26029,20 +26507,118 @@ } }, "@radix-ui/react-accordion": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz", - "integrity": "sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-collapsible": "1.0.2", - "@radix-ui/react-collection": "1.0.2", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + } } }, "@radix-ui/react-alert-dialog": { @@ -26085,19 +26661,107 @@ } }, "@radix-ui/react-collapsible": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz", - "integrity": "sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, + "@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + } } }, "@radix-ui/react-collection": { @@ -28759,6 +29423,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", @@ -29108,6 +29778,37 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "requires": { + "@ucast/core": "^1.0.0" + } + }, + "@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "requires": { + "@ucast/core": "^1.4.1" + } + }, + "@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "requires": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -40082,6 +40783,11 @@ "property-expr": "^2.0.4", "toposort": "^2.0.2" } + }, + "zod": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", + "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==" } } } diff --git a/frontend/package.json b/frontend/package.json index fab8b7033..d954da2a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "build-storybook": "storybook build" }, "dependencies": { + "@casl/ability": "^6.5.0", + "@casl/react": "^3.1.0", "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", @@ -27,7 +29,7 @@ "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^2.9.10", "@octokit/rest": "^19.0.7", - "@radix-ui/react-accordion": "^1.1.0", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -47,6 +49,7 @@ "@stripe/stripe-js": "^1.46.0", "@tanstack/react-query": "^4.23.0", "@types/argon2-browser": "^1.18.1", + "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -71,6 +74,7 @@ "markdown-it": "^13.0.1", "next": "^12.3.4", "nprogress": "^0.2.0", + "picomatch": "^2.3.1", "posthog-js": "^1.58.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -96,7 +100,8 @@ "uuid": "^8.3.2", "uuidv4": "^6.2.13", "yaml": "^2.2.2", - "yup": "^0.32.11" + "yup": "^0.32.11", + "zod": "^3.22.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.23", @@ -111,6 +116,7 @@ "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", + "@types/picomatch": "^2.3.0", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx index a8b965269..77ca8c24f 100644 --- a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -1,78 +1,92 @@ - import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Checkbox, PopoverContent } from "@app/components/v2"; +import { Button, Checkbox, PopoverContent } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { WsTag } from "../../hooks/api/tags/types"; +import { ProjectPermissionCan } from "../permissions"; interface Props { - wsTags: WsTag[] | undefined; - secKey: string; - selectedTagIds: Record; - handleSelectTag: (wsTag: WsTag) => void; - handleTagOnMouseEnter: (wsTag: WsTag) => void; - handleTagOnMouseLeave: () => void; - checkIfTagIsVisible: (wsTag: WsTag) => boolean; - handleOnCreateTagOpen: () => void + wsTags: WsTag[] | undefined; + secKey: string; + selectedTagIds: Record; + handleSelectTag: (wsTag: WsTag) => void; + handleTagOnMouseEnter: (wsTag: WsTag) => void; + handleTagOnMouseLeave: () => void; + checkIfTagIsVisible: (wsTag: WsTag) => boolean; + handleOnCreateTagOpen: () => void; } const AddTagPopoverContent = ({ - wsTags, - secKey, - selectedTagIds, - handleSelectTag, - handleTagOnMouseEnter, - handleTagOnMouseLeave, - checkIfTagIsVisible, - handleOnCreateTagOpen + wsTags, + secKey, + selectedTagIds, + handleSelectTag, + handleTagOnMouseEnter, + handleTagOnMouseLeave, + checkIfTagIsVisible, + handleOnCreateTagOpen }: Props) => { - return ( - -
- Add tags to {secKey || "this secret"} + return ( + +
+ Add tags to {secKey || "this secret"} +
+
+
+ {wsTags?.map((wsTag: WsTag) => ( +
handleSelectTag(wsTag)} + onMouseEnter={() => handleTagOnMouseEnter(wsTag)} + onMouseLeave={() => handleTagOnMouseLeave()} + tabIndex={0} + role="button" + onKeyDown={() => {}} + > + {(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && ( + + )} +
+
+ {" "} +
+ {wsTag.slug}
-
-
- {wsTags?.map((wsTag: WsTag) => ( -
handleSelectTag(wsTag)} - onMouseEnter={() => handleTagOnMouseEnter(wsTag)} - onMouseLeave={() => handleTagOnMouseLeave()} - tabIndex={0} role="button" - onKeyDown={() => { }}> - { +
+ ))} + + {(isAllowed) => ( + + )} + +
+ + ); +}; - (checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && - } -
-
- - {wsTag.slug} - -
-
- ))} -
handleOnCreateTagOpen()} - tabIndex={0} role="button" - onKeyDown={() => { }}> - - Add new tag -
-
- - ) -} - -export default AddTagPopoverContent \ No newline at end of file +export default AddTagPopoverContent; diff --git a/frontend/src/components/permissions/OrgPermissionCan.tsx b/frontend/src/components/permissions/OrgPermissionCan.tsx new file mode 100644 index 000000000..4366110a9 --- /dev/null +++ b/frontend/src/components/permissions/OrgPermissionCan.tsx @@ -0,0 +1,49 @@ +import { FunctionComponent, ReactNode } from "react"; +import { BoundCanProps, Can } from "@casl/react"; + +import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext"; + +import { Tooltip } from "../v2"; + +type Props = { + label?: ReactNode; + // this prop is used when there exist already a tooltip as helper text for users + // so when permission is allowed same tooltip will be reused to show helpertext + renderTooltip?: boolean; + allowedLabel?: string; +} & BoundCanProps; + +export const OrgPermissionCan: FunctionComponent = ({ + label = "Access restricted", + children, + passThrough = true, + renderTooltip, + allowedLabel, + ...props +}) => { + const permission = useOrgPermission(); + + return ( + + {(isAllowed, ability) => { + // akhilmhdh: This is set as type due to error in casl react type. + const finalChild = + typeof children === "function" + ? children(isAllowed, ability as TOrgPermission) + : children; + + if (!isAllowed && passThrough) { + return {finalChild}; + } + + if (isAllowed && renderTooltip) { + return {finalChild}; + } + + if (!isAllowed) return null; + + return finalChild; + }} + + ); +}; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx new file mode 100644 index 000000000..40e17577f --- /dev/null +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from "react"; +import { faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +type Props = { + containerClassName?: string; + className?: string; + children?: ReactNode; +}; + +export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => { + return ( +
+
+
+ +
+
+
Access Restricted
+ {children || ( +
+ Your role has limited permissions, please
contact your administrator to gain access +
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx new file mode 100644 index 000000000..aa64c33d7 --- /dev/null +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -0,0 +1,52 @@ +import { FunctionComponent, ReactNode } from "react"; +import { BoundCanProps, Can } from "@casl/react"; + +import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext"; + +import { Tooltip } from "../v2"; + +type Props = { + label?: ReactNode; + // this prop is used when there exist already a tooltip as helper text for users + // so when permission is allowed same tooltip will be reused to show helpertext + renderTooltip?: boolean; + allowedLabel?: string; + // BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission + // For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error + // tried a lot combinations + // REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15 +} & BoundCanProps; + +export const ProjectPermissionCan: FunctionComponent = ({ + label = "Access restricted", + children, + passThrough = true, + renderTooltip, + allowedLabel, + ...props +}) => { + const permission = useProjectPermission(); + return ( + + {(isAllowed, ability) => { + // akhilmhdh: This is set as type due to error in casl react type. + const finalChild = + typeof children === "function" + ? children(isAllowed, ability as TProjectPermission) + : children; + + if (!isAllowed && passThrough) { + return {finalChild}; + } + + if (isAllowed && renderTooltip) { + return {finalChild}; + } + + if (!isAllowed) return null; + + return finalChild; + }} + + ); +}; diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx new file mode 100644 index 000000000..8d523c311 --- /dev/null +++ b/frontend/src/components/permissions/index.tsx @@ -0,0 +1,3 @@ +export { OrgPermissionCan } from "./OrgPermissionCan"; +export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; +export { ProjectPermissionCan } from "./ProjectPermissionCan"; diff --git a/frontend/src/components/v2/Accordion/Accordion.tsx b/frontend/src/components/v2/Accordion/Accordion.tsx new file mode 100644 index 000000000..20ec5bacd --- /dev/null +++ b/frontend/src/components/v2/Accordion/Accordion.tsx @@ -0,0 +1,77 @@ +import { forwardRef } from "react"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { twMerge } from "tailwind-merge"; + +export const AccordionItem = forwardRef( + ({ children, className, ...props }, forwardedRef) => ( + + {children} + + ) +); +AccordionItem.displayName = "AccordionItem"; + +export const AccordionTrigger = forwardRef< + HTMLButtonElement, + AccordionPrimitive.AccordionTriggerProps +>(({ children, className, ...props }, forwardedRef) => ( + + + {children} + + + +)); + +AccordionTrigger.displayName = "AccordionTrigger"; + +export const AccordionContent = forwardRef< + HTMLDivElement, + AccordionPrimitive.AccordionContentProps +>(({ children, className, ...props }, forwardedRef) => ( + +
{children}
+
+)); + +AccordionContent.displayName = "AccordionContent"; + +// ref: https://www.radix-ui.com/primitives/docs/components/accordion#root +export const Accordion = ({ + children, + ...props +}: AccordionPrimitive.AccordionSingleProps | AccordionPrimitive.AccordionMultipleProps) => ( + + {children} + +); diff --git a/frontend/src/components/v2/Accordion/index.tsx b/frontend/src/components/v2/Accordion/index.tsx new file mode 100644 index 000000000..831904d54 --- /dev/null +++ b/frontend/src/components/v2/Accordion/index.tsx @@ -0,0 +1 @@ +export { Accordion, AccordionContent, AccordionItem,AccordionTrigger } from "./Accordion"; diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 64ec0a54a..8c79a5ff2 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -45,7 +45,7 @@ export const Checkbox = ({ -

- + {(isAllowed) => ( + + )} +
@@ -723,16 +739,26 @@ export const AppLayout = ({ children }: LayoutProps) => { ( - - Add all members of my organization to this project - + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} + )} />
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 56fb8c9f7..45a1cbe71 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -17,7 +17,9 @@ import { TooltipProvider } from "@app/components/v2"; import { publicPaths } from "@app/const"; import { AuthProvider, + OrgPermissionProvider, OrgProvider, + ProjectPermissionProvider, SubscriptionProvider, UserProvider, WorkspaceProvider @@ -95,17 +97,21 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/org/[id]/billing/index.tsx b/frontend/src/pages/org/[id]/billing/index.tsx index a752d93f0..12e71e69a 100644 --- a/frontend/src/pages/org/[id]/billing/index.tsx +++ b/frontend/src/pages/org/[id]/billing/index.tsx @@ -1,21 +1,28 @@ import { useTranslation } from "react-i18next"; import Head from "next/head"; +import { OrgPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context"; +import { withPermission } from "@app/hoc"; import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage"; -export default function SettingsBilling() { - const { t } = useTranslation(); +const SettingsBilling = withPermission<{}, TOrgPermission>( + () => { + const { t } = useTranslation(); - return ( -
- - {t("common.head-title", { title: t("billing.title") })} - - - - -
- ); -} + return ( +
+ + {t("common.head-title", { title: t("billing.title") })} + + + + +
+ ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing } +); -SettingsBilling.requireAuth = true; \ No newline at end of file +Object.assign(SettingsBilling, { requireAuth: true }); + +export default SettingsBilling; diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 3f9355863..fdd5c2cdf 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -1,3 +1,5 @@ +// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex + import crypto from "crypto"; import { useEffect, useState } from "react"; @@ -11,7 +13,10 @@ import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faFolderOpen } from "@fortawesome/free-regular-svg-icons"; import { faArrowRight, + faArrowUpRightFromSquare, + faCheck, faCheckCircle, + faClipboard, faExclamationCircle, faHandPeace, faMagnifyingGlass, @@ -22,9 +27,11 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; +import * as Tabs from "@radix-ui/react-tabs"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { OrgPermissionCan } from "@app/components/permissions"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import { Button, @@ -36,9 +43,21 @@ import { Skeleton, UpgradePlanModal } from "@app/components/v2"; -import { TabsObject } from "@app/components/v2/Tabs"; -import { useSubscription, useUser, useWorkspace } from "@app/context"; -import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useSubscription, + useUser, + useWorkspace +} from "@app/context"; +import { withPermission } from "@app/hoc"; +import { + fetchOrgUsers, + useAddUserToWs, + useCreateWorkspace, + useRegisterUserAction, + useUploadWsKey +} from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -63,6 +82,210 @@ type ItemProps = { link?: string; }; +function copyToClipboard(id: string, setState: (value: boolean) => void) { + // Get the text field + const copyText = document.getElementById(id) as HTMLInputElement; + + // Select the text field + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.value); + + setState(true); + setTimeout(() => setState(false), 2000); + // Alert the copied text + // alert("Copied the text: " + copyText.value); +} + +const CodeItem = ({ + isCopied, + setIsCopied, + textExplanation, + code, + id +}: { + isCopied: boolean; + setIsCopied: (value: boolean) => void; + textExplanation: string; + code: string; + id: string; +}) => { + return ( + <> +

{textExplanation}

+
+ + +
+ + ); +}; + +const TabsObject = () => { + const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); + const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); + const [loginCodeCopied, setLoginCodeCopied] = useState(false); + const [initCodeCopied, setInitCodeCopied] = useState(false); + const [runCodeCopied, setRunCodeCopied] = useState(false); + + return ( + + + + MacOS + + + Windows + + {/* + Arch Linux + */} + + Other Platforms + + + + + + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+ + +
+ + +
+ + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+
+ ); +}; + const LearningItem = ({ text, subText, @@ -92,9 +315,7 @@ const LearningItem = ({ tabIndex={0} onClick={async () => { if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction.mutateAsync( - userAction - ); + await registerUserAction.mutateAsync(userAction); } }} className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ @@ -237,410 +458,446 @@ type TAddProjectFormData = yup.InferType; // #TODO: Update all the workspaceIds -export default function Organization() { - const { t } = useTranslation(); +const OrganizationPage = withPermission( + () => { + const { t } = useTranslation(); - const router = useRouter(); + const router = useRouter(); - const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); - const orgWorkspaces = - workspaces?.filter( - (workspace) => workspace.organization === localStorage.getItem("orgData.id") - ) || []; - const currentOrg = String(router.query.id); - const { createNotification } = useNotificationContext(); - const addWsUser = useAddUserToWs(); + const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); + const orgWorkspaces = + workspaces?.filter( + (workspace) => workspace.organization === localStorage.getItem("orgData.id") + ) || []; + const currentOrg = String(router.query.id); + const { createNotification } = useNotificationContext(); + const addWsUser = useAddUserToWs(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "addNewWs", - "upgradePlan" - ] as const); - const { - control, - formState: { isSubmitting }, - reset, - handleSubmit - } = useForm({ - resolver: yupResolver(formSchema) - }); - - const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); - const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); - const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); - const [usersInOrg, setUsersInOrg] = useState(false); - const [searchFilter, setSearchFilter] = useState(""); - const createWs = useCreateWorkspace(); - const { user } = useUser(); - const uploadWsKey = useUploadWsKey(); - const { data: serverDetails } = useFetchServerStatus(); - - - const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { - // type check - if (!currentOrg) return; - try { - const { - data: { - workspace: { _id: newWorkspaceId } - } - } = await createWs.mutateAsync({ - organizationId: currentOrg, - workspaceName: name - }); - - const randomBytes = crypto.randomBytes(16).toString("hex"); - const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: user.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey.mutateAsync({ - encryptedKey: ciphertext, - nonce, - userId: user?._id, - workspaceId: newWorkspaceId - }); - - if (addMembers) { - // not using hooks because need at this point only - const orgUsers = await fetchOrgUsers(currentOrg); - orgUsers.forEach(({ status, user: orgUser }) => { - // skip if status of org user is not accepted - // this orgUser is the person who created the ws - if (status !== "accepted" || user.email === orgUser.email) return; - addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); - }); - } - createNotification({ text: "Workspace created", type: "success" }); - handlePopUpClose("addNewWs"); - router.push(`/project/${newWorkspaceId}/secrets/overview`); - } catch (err) { - console.error(err); - createNotification({ text: "Failed to create workspace", type: "error" }); - } - }; - - const { subscription } = useSubscription(); - - const isAddingProjectsAllowed = subscription?.workspaceLimit - ? subscription.workspacesUsed < subscription.workspaceLimit - : true; - - useEffect(() => { - onboardingCheck({ - setHasUserClickedIntro, - setHasUserClickedSlack, - setHasUserPushedSecrets, - setUsersInOrg + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addNewWs", + "upgradePlan" + ] as const); + const { + control, + formState: { isSubmitting }, + reset, + handleSubmit + } = useForm({ + resolver: yupResolver(formSchema) }); - }, []); - const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; + const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); + const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); + const [usersInOrg, setUsersInOrg] = useState(false); + const [searchFilter, setSearchFilter] = useState(""); + const createWs = useCreateWorkspace(); + const { user } = useUser(); + const uploadWsKey = useUploadWsKey(); + const { data: serverDetails } = useFetchServerStatus(); - return ( -
- - {t("common.head-title", { title: t("settings.members.title") })} - - - {!serverDetails?.redisConfigured &&
-

Announcements

-
- - Attention: Updated versions of Infisical now require Redis for full functionality. Learn how to configure it - - - here - - . + const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { + // type check + if (!currentOrg) return; + try { + const { + data: { + workspace: { _id: newWorkspaceId } + } + } = await createWs.mutateAsync({ + organizationId: currentOrg, + workspaceName: name + }); + + const randomBytes = crypto.randomBytes(16).toString("hex"); + const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: randomBytes, + publicKey: user.publicKey, + privateKey: PRIVATE_KEY + }); + + await uploadWsKey.mutateAsync({ + encryptedKey: ciphertext, + nonce, + userId: user?._id, + workspaceId: newWorkspaceId + }); + + if (addMembers) { + // not using hooks because need at this point only + const orgUsers = await fetchOrgUsers(currentOrg); + orgUsers.forEach(({ status, user: orgUser }) => { + // skip if status of org user is not accepted + // this orgUser is the person who created the ws + if (status !== "accepted" || user.email === orgUser.email) return; + addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); + }); + } + createNotification({ text: "Workspace created", type: "success" }); + handlePopUpClose("addNewWs"); + router.push(`/project/${newWorkspaceId}/secrets/overview`); + } catch (err) { + console.error(err); + createNotification({ text: "Failed to create workspace", type: "error" }); + } + }; + + const { subscription } = useSubscription(); + + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; + + useEffect(() => { + onboardingCheck({ + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setUsersInOrg + }); + }, []); + + const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; + + return ( +
+ + {t("common.head-title", { title: t("settings.members.title") })} + + + {!serverDetails?.redisConfigured && ( +
+

Announcements

+
+ + Attention: Updated versions of Infisical now require Redis for full functionality. + Learn how to configure it + + + here + + + . +
-
} -
-

Projects

-
- setSearchFilter(e.target.value)} - leftIcon={} - /> - -
-
- {isWorkspaceLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
-
- -
-
- -
-
- -
-
- ))} - {orgWorkspaces - .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) - .map((workspace) => ( -
-
{workspace.name}
-
- {workspace.environments?.length || 0} environments -
- + )} + +
+
+ {isWorkspaceLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+
+ +
+
+ +
+
+ +
+
+ ))} + {orgWorkspaces + .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) + .map((workspace) => ( +
+
{workspace.name}
+
+ {workspace.environments?.length || 0} environments +
+ +
+ ))} +
+ {isWorkspaceEmpty && ( +
+ +
+ You are not part of any projects in this organization yet. When you are, they will + appear here. +
+
+ Create a new project, or ask other organization members to give you necessary + permissions. +
+
+ )} +
+ {new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && ( +
+

Onboarding Guide

+
+ + {orgWorkspaces.length !== 0 && ( + <> + + + + )} +
+ +
+
+ {orgWorkspaces.length !== 0 && ( +
+
+
+ + {false && ( +
+ +
+ )} +
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative. +
+
+
+
+ About 2 min +
+
+ + {false &&
} +
+ )} + {orgWorkspaces.length !== 0 && ( + + )} +
+ )} +
+

Explore More

+
+ {features.map((feature) => ( +
+
{feature.name}
+
+ {feature.description} +
+
+
+ Setup time: 20 min +
+ + Learn more{" "} -
- + +
))} -
- {isWorkspaceEmpty && ( -
- -
- You are not part of any projects in this organization yet. When you are, they will - appear here. -
-
- Create a new project, or ask other organization members to give you necessary - permissions. -
- )} -
- {new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && ( -
-

Onboarding Guide

-
- - {orgWorkspaces.length !== 0 && ( - <> - - - - )} -
- -
-
- {orgWorkspaces.length !== 0 && ( -
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
-
-
-
- About 2 min -
-
- - {false &&
} -
- )} - {orgWorkspaces.length !== 0 && ( - - )}
- )} -
-

Explore More

-
{ + handlePopUpToggle("addNewWs", isModalOpen); + reset(); + }} > - {features.map((feature) => ( -
-
{feature.name}
-
- {feature.description} -
-
-
Setup time: 20 min
- - Learn more{" "} - - -
-
- ))} -
-
- { - handlePopUpToggle("addNewWs", isModalOpen); - reset(); - }} - > - -
- ( - - - - )} - /> -
+ + ( - ( + - Add all members of my organization to this project - + + )} /> -
-
- - -
- -
-
- handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> - {/* */} -
- ); -} +
+ ( + + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} +
+ )} + /> +
+
+ + +
+ + + + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> + {/* */} +
+ ); + }, + { + action: OrgPermissionActions.Read, + subject: OrgPermissionSubjects.Workspace + } +); -Organization.requireAuth = true; +Object.assign(OrganizationPage, { requireAuth: true }); + +export default OrganizationPage; diff --git a/frontend/src/pages/org/[id]/secret-scanning/index.tsx b/frontend/src/pages/org/[id]/secret-scanning/index.tsx index e0f98243a..5dc5ffe1e 100644 --- a/frontend/src/pages/org/[id]/secret-scanning/index.tsx +++ b/frontend/src/pages/org/[id]/secret-scanning/index.tsx @@ -1,89 +1,133 @@ import { useEffect, useState } from "react"; import Head from "next/head"; -import { useRouter } from "next/router" +import { useRouter } from "next/router"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; import { SecretScanningLogsTable } from "@app/views/SecretScanning/components"; import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession"; import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus"; import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization"; -export default function SecretScanning() { - const router = useRouter() - const queryParams = router.query - const [integrationEnabled, setIntegrationStatus] = useState(false) +const SecretScanning = withPermission( + () => { + const router = useRouter(); + const queryParams = router.query; + const [integrationEnabled, setIntegrationStatus] = useState(false); - useEffect(()=>{ - const linkInstallation = async () => { - if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){ - try { - const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string) - if (isLinked){ - router.reload() + useEffect(() => { + const linkInstallation = async () => { + if ( + typeof queryParams.state === "string" && + typeof queryParams.installation_id === "string" + ) { + try { + const isLinked = await linkGitAppInstallationWithOrganization( + queryParams.installation_id as string, + queryParams.state as string + ); + if (isLinked) { + router.reload(); + } + + console.log("installation verification complete"); + } catch (e) { + console.log("app installation is stale, start new session", e); } - - console.log("installation verification complete") - }catch (e){ - console.log("app installation is stale, start new session", e) } - } - } + }; - const fetchInstallationStatus = async () => { - const status = await getInstallationStatus(String(localStorage.getItem("orgData.id"))) - setIntegrationStatus(status) - } + const fetchInstallationStatus = async () => { + const status = await getInstallationStatus(String(localStorage.getItem("orgData.id"))); + setIntegrationStatus(status); + }; - fetchInstallationStatus() - linkInstallation() - },[queryParams.state, queryParams.installation_id]) + fetchInstallationStatus(); + linkInstallation(); + }, [queryParams.state, queryParams.installation_id]); - const generateNewIntegrationSession = async () => { - const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id"))) - router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`) - } + const generateNewIntegrationSession = async () => { + const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id"))); + router.push( + `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}` + ); + }; - return ( -
- - Secret scanning - - - -
-
-
Secret Scanning
-
Automatically monitor your GitHub activity and prevent secret leaks
-
-
-
Secret Scanning Status: {integrationEnabled ?

Enabled

:

Not enabled

}
-
{integrationEnabled ?

Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.

:

Connect your GitHub organization to Infisical.

}
+ return ( +
+ + Secret scanning + + + +
+
+
Secret Scanning
+
+ Automatically monitor your GitHub activity and prevent secret leaks
- {integrationEnabled ? ( -
-
-
-
+
+
+
+ Secret Scanning Status:{" "} + {integrationEnabled ? ( +

Enabled

+ ) : ( +

Not enabled

+ )} +
+
+ {integrationEnabled ? ( +

+ Your GitHub organization is connected to Infisical, and is being continuously + monitored for secret leaks. +

+ ) : ( +

+ Connect your GitHub organization to Infisical. +

+ )} +
- ) : ( -
- -
- )} + {integrationEnabled ? ( +
+
+
+
+
+ ) : ( +
+ + {(isAllowed) => ( + + )} + +
+ )} +
+
-
-
- ); -} + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning } +); -SecretScanning.requireAuth = true; +Object.assign(SecretScanning, { requireAuth: true }); + +export default SecretScanning; diff --git a/frontend/src/pages/project/[id]/logs/index.tsx b/frontend/src/pages/project/[id]/logs/index.tsx index 0c44abbef..cc6f79457 100644 --- a/frontend/src/pages/project/[id]/logs/index.tsx +++ b/frontend/src/pages/project/[id]/logs/index.tsx @@ -6,8 +6,9 @@ import { useRouter } from "next/router"; import Button from "@app/components/basic/buttons/Button"; import EventFilter from "@app/components/basic/EventFilter"; import { UpgradePlanModal } from "@app/components/v2"; -import { useSubscription } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context"; import ActivitySideBar from "@app/ee/components/ActivitySideBar"; +import { withProjectPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks/usePopUp"; import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs"; @@ -23,10 +24,10 @@ interface LogData { }; serviceAccount?: { string: string; - }, + }; serviceTokenData?: { name: string; - } + }; actions: { _id: string; name: string; @@ -60,67 +61,33 @@ interface LogDataPoint { /** * This is the tab that includes all of the user activity logs */ -export default function Activity() { - const router = useRouter(); - const [eventChosen, setEventChosen] = useState(""); - const [logsData, setLogsData] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [currentOffset, setCurrentOffset] = useState(0); - const currentLimit = 10; - const [currentSidebarAction, toggleSidebar] = useState(); - const { t } = useTranslation(); - const { subscription } = useSubscription(); - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "upgradePlan" - ] as const); +const Activity = withProjectPermission( + () => { + const router = useRouter(); + const [eventChosen, setEventChosen] = useState(""); + const [logsData, setLogsData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [currentOffset, setCurrentOffset] = useState(0); + const currentLimit = 10; + const [currentSidebarAction, toggleSidebar] = useState(); + const { t } = useTranslation(); + const { subscription } = useSubscription(); + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["upgradePlan"] as const); - // this use effect updates the data in case of a new filter being added - useEffect(() => { - setCurrentOffset(0); - const getLogData = async () => { - setIsLoading(true); - const tempLogsData = await getProjectLogs({ - workspaceId: String(router.query.id), - offset: 0, - limit: currentLimit, - userId: "", - actionNames: eventChosen - }); + // this use effect updates the data in case of a new filter being added + useEffect(() => { + setCurrentOffset(0); + const getLogData = async () => { + setIsLoading(true); + const tempLogsData = await getProjectLogs({ + workspaceId: String(router.query.id), + offset: 0, + limit: currentLimit, + userId: "", + actionNames: eventChosen + }); - setLogsData( - tempLogsData.map((log: LogData) => ({ - _id: log._id, - channel: log.channel, - createdAt: log.createdAt, - ipAddress: log.ipAddress, - user: log?.user?.email, - serviceAccount: log?.serviceAccount, - serviceTokenData: log?.serviceTokenData, - payload: log.actions.map((action) => ({ - _id: action._id, - name: action.name, - secretVersions: action.payload.secretVersions - })) - })) - ); - setIsLoading(false); - }; - getLogData(); - }, [eventChosen]); - - // this use effect adds more data in case 'View More' button is clicked - useEffect(() => { - const getLogData = async () => { - setIsLoading(true); - const tempLogsData = await getProjectLogs({ - workspaceId: String(router.query.id), - offset: currentOffset, - limit: currentLimit, - userId: "", - actionNames: eventChosen - }); - setLogsData( - logsData.concat( + setLogsData( tempLogsData.map((log: LogData) => ({ _id: log._id, channel: log.channel, @@ -135,63 +102,103 @@ export default function Activity() { secretVersions: action.payload.secretVersions })) })) - ) - ); - setIsLoading(false); + ); + setIsLoading(false); + }; + getLogData(); + }, [eventChosen]); + + // this use effect adds more data in case 'View More' button is clicked + useEffect(() => { + const getLogData = async () => { + setIsLoading(true); + const tempLogsData = await getProjectLogs({ + workspaceId: String(router.query.id), + offset: currentOffset, + limit: currentLimit, + userId: "", + actionNames: eventChosen + }); + setLogsData( + logsData.concat( + tempLogsData.map((log: LogData) => ({ + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log?.user?.email, + serviceAccount: log?.serviceAccount, + serviceTokenData: log?.serviceTokenData, + payload: log.actions.map((action) => ({ + _id: action._id, + name: action.name, + secretVersions: action.payload.secretVersions + })) + })) + ) + ); + setIsLoading(false); + }; + getLogData(); + }, [currentLimit, currentOffset]); + + const loadMoreLogs = () => { + if (subscription?.auditLogs === false) { + handlePopUpOpen("upgradePlan"); + } else { + setCurrentOffset(currentOffset + currentLimit); + } }; - getLogData(); - }, [currentLimit, currentOffset]); - const loadMoreLogs = () => { - if (subscription?.auditLogs === false) { - handlePopUpOpen("upgradePlan"); - } else { - setCurrentOffset(currentOffset + currentLimit); - } - }; - - return ( -
- - Audit Logs - - - - {currentSidebarAction && ( - - )} -
-
-

{t("activity.title")}

+ return ( +
+ + Audit Logs + + + + {currentSidebarAction && ( + + )} +
+
+

{t("activity.title")}

+
+

{t("activity.subtitle")}

-

{t("activity.subtitle")}

-
-
- -
- -
-
-
+ +
+
+
+
+ {subscription && ( + handlePopUpClose("upgradePlan")} + text={ + subscription.slug === null + ? "You can see more logs under an Enterprise license" + : "You can see more logs if you switch to Infisical's Business/Professional Plan." + } /> -
+ )}
- {subscription && ( - handlePopUpClose("upgradePlan")} - text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."} - /> - )} -
- ); -} + ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs } +); -Activity.requireAuth = true; +Object.assign(Activity, { requireAuth: true }); +export default Activity; diff --git a/frontend/src/pages/project/[id]/members/index.tsx b/frontend/src/pages/project/[id]/members/index.tsx index 997606b1d..4bcbb833e 100644 --- a/frontend/src/pages/project/[id]/members/index.tsx +++ b/frontend/src/pages/project/[id]/members/index.tsx @@ -1,225 +1,21 @@ -import { useEffect, useState } from "react"; +/* eslint-disable @typescript-eslint/no-unused-vars */ import { useTranslation } from "react-i18next"; import Head from "next/head"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Button from "@app/components/basic/buttons/Button"; -import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMemberDialog"; -import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable"; -import guidGenerator from "@app/components/utilities/randomId"; -import { Input } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useAddUserToWorkspace, - useGetOrgUsers, - useGetUser, - useGetWorkspaceUsers} from "@app/hooks/api"; -import { uploadWsKey } from "@app/hooks/api/keys/queries"; - -import { - decryptAssymmetric, - encryptAssymmetric -} from "../../../../components/utilities/cryptography/crypto"; - -interface UserProps { - firstName: string; - lastName: string; - email: string; - _id: string; - publicKey: string; -} - -interface MembershipProps { - deniedPermissions: any[]; - user: UserProps; - inviteEmail: string; - role: string; - status: string; - _id: string; -} - -// #TODO: Update all the workspaceIds - -export default function Users() { - const router = useRouter(); - const workspaceId = router.query.id as string; - - const { data: user } = useGetUser(); - const { currentOrg } = useOrganization(); - const { data: orgUsers } = useGetOrgUsers(currentOrg?._id ?? ""); - - const { data: workspaceUsers } = useGetWorkspaceUsers(workspaceId); - const { mutateAsync: addUserToWorkspaceMutateAsync } = useAddUserToWorkspace(); - - const [isAddOpen, setIsAddOpen] = useState(false); - // let [isDeleteOpen, setIsDeleteOpen] = useState(false); - // let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false); - const [email, setEmail] = useState(""); - const [personalEmail, setPersonalEmail] = useState(""); - const [searchUsers, setSearchUsers] = useState(""); +import { MembersPage } from "@app/views/Project/MembersPage"; +export default function WorkspaceMemberSettings() { const { t } = useTranslation(); - - const [userList, setUserList] = useState([]); - const [isUserListLoading, setIsUserListLoading] = useState(true); - const [orgUserList, setOrgUserList] = useState([]); - - useEffect(() => { - if (user && workspaceUsers && orgUsers) { - (async () => { - setPersonalEmail(user.email); - - const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({ - key: guidGenerator(), - firstName: membership.user?.firstName, - lastName: membership.user?.lastName, - email: membership.user?.email === null ? membership.inviteEmail : membership.user?.email, - role: membership?.role, - status: membership?.status, - userId: membership.user?._id, - membershipId: membership._id, - deniedPermissions: membership.deniedPermissions, - publicKey: membership.user?.publicKey - })); - setUserList(tempUserList); - - setIsUserListLoading(false); - - setOrgUserList(orgUsers); - setEmail( - orgUsers - ?.filter((membership: MembershipProps) => membership.status === "accepted") - .map((membership: MembershipProps) => membership.user.email) - .filter( - (usEmail: string) => - !tempUserList?.map((user1: UserProps) => user1.email).includes(usEmail) - )[0] - ); - })(); - } - }, [user, workspaceUsers, orgUsers]); - - const closeAddModal = () => { - setIsAddOpen(false); - }; - - const openAddModal = () => { - setIsAddOpen(true); - }; - - // function closeDeleteModal() { - // setIsDeleteOpen(false); - // } - - // function deleteMembership(userId) { - // deleteUserFromWorkspace(userId, router.query.id) - // } - - // function openDeleteModal() { - // setIsDeleteOpen(true); - // } - - const submitAddModal = async () => { - const result = await addUserToWorkspaceMutateAsync({ - email, - workspaceId - }); - - if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: result.latestKey.encryptedKey, - nonce: result.latestKey.nonce, - publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey({ - workspaceId, - userId: result.invitee._id, - encryptedKey: ciphertext, - nonce - }); - } - setEmail(""); - setIsAddOpen(false); - }; - - return userList ? ( -
+ return ( + <> {t("common.head-title", { title: t("settings.members.title") })} -
-

{t("settings.members.title")}

-
- membership.status === "accepted") - .map((membership: MembershipProps) => membership.user.email) - .filter( - (orgEmail) => !userList?.map((user1: UserProps) => user1.email).includes(orgEmail) - )} - setEmail={setEmail} - /> - {/* */} -
-
- setSearchUsers(e.target.value)} - leftIcon={} - /> -
-
-
-
-
- -
-
- ) : ( -
- loading animation -
+ + ); } -Users.requireAuth = true; +WorkspaceMemberSettings.requireAuth = true; diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 6d5070fe0..fe1015485 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -1,6 +1,10 @@ @tailwind base; @tailwind components; +html { + @apply overflow-hidden; +} + .rdp-day, .rdp-nav_button { @apply rounded-md hover:text-mineshaft-500; @@ -108,7 +112,13 @@ } } .tags-conic-bg { - background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122)); + background: conic-gradient( + rgb(235, 87, 87), + rgb(242, 201, 76), + rgb(76, 183, 130), + rgb(78, 167, 252), + rgb(250, 96, 122) + ); } .show-tags { diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index faaaeb49c..81cbedf03 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; +import { subject } from "@casl/ability"; import { closestCenter, DndContext, @@ -39,6 +40,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import NavHeader from "@app/components/navigation/NavHeader"; +import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -55,7 +57,14 @@ import { UpgradePlanModal } from "@app/components/v2"; import { leaveConfirmDefaultMessage } from "@app/const"; -import { useOrganization, useSubscription, useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; import { useBatchSecretsOp, @@ -71,7 +80,6 @@ import { useGetSecretVersion, useGetSnapshotSecrets, useGetUserAction, - useGetUserWsEnvironments, useGetUserWsKey, useGetWorkspaceSecretSnapshots, useGetWsSnapshotCount, @@ -82,7 +90,6 @@ import { useUpdateSecretImport } from "@app/hooks/api"; import { secretKeys } from "@app/hooks/api/secrets/queries"; -import { WorkspaceEnv } from "@app/hooks/api/types"; import { CompareSecret } from "./components/CompareSecret"; import { CreateTagModal } from "./components/CreateTagModal"; @@ -129,7 +136,8 @@ export const DashboardPage = () => { const router = useRouter(); const { createNotification } = useNotificationContext(); const queryClient = useQueryClient(); - const envQuery = router.query.env as string; + const environment = router.query.env as string; + const permission = useProjectPermission(); const secretContainer = useRef(null); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ @@ -147,9 +155,8 @@ export const DashboardPage = () => { const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [searchFilter, setSearchFilter] = useState(""); const [snapshotId, setSnaphotId] = useState(null); - const [selectedEnv, setSelectedEnv] = useState(null); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); - const deletedSecretIds = useRef<{ id: string; secretName: string; }[]>([]); + const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]); const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); const folderId = router.query.folderId as string; @@ -158,7 +165,6 @@ export const DashboardPage = () => { const { currentWorkspace, isLoading } = useWorkspace(); const { currentOrg } = useOrganization(); const workspaceId = currentWorkspace?._id as string; - const selectedEnvSlug = selectedEnv?.slug || ""; const { data: latestFileKey } = useGetUserWsKey(workspaceId); @@ -172,17 +178,6 @@ export const DashboardPage = () => { const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); const hasUserPushed = Boolean(userAction); - const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId, - onSuccess: (data) => { - // get an env with one of the access available - const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); - if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) { - setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]); - } - } - }); - const { data: secretVersion } = useGetSecretVersion({ limit: 10, offset: 0, @@ -192,7 +187,7 @@ export const DashboardPage = () => { const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ workspaceId, - env: selectedEnvSlug, + env: environment, decryptFileKey: latestFileKey!, isPaused: Boolean(snapshotId), folderId @@ -200,7 +195,7 @@ export const DashboardPage = () => { const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ workspaceId: workspaceId || "", - environment: selectedEnvSlug, + environment, parentFolderId: folderId, isPaused: isRollbackMode, sortDir @@ -213,7 +208,7 @@ export const DashboardPage = () => { isFetchingNextPage } = useGetWorkspaceSecretSnapshots({ workspaceId, - environment: selectedEnvSlug, + environment, folder: folderId, limit: 10 }); @@ -224,13 +219,13 @@ export const DashboardPage = () => { isFetching: isSnapshotChanging } = useGetSnapshotSecrets({ snapshotId: snapshotId || "", - env: selectedEnvSlug, + env: environment, decryptFileKey: latestFileKey! }); const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( workspaceId, - selectedEnvSlug, + environment, folderId ); @@ -247,17 +242,38 @@ export const DashboardPage = () => { const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports( workspaceId, - selectedEnvSlug, + environment, folderId ); const { data: importedSecrets } = useGetImportedSecrets({ workspaceId, decryptFileKey: latestFileKey!, - environment: selectedEnvSlug, + environment, folderId }); + const secretPath = `/${(folderData?.dir || []) + ?.filter(({ name }) => name !== "root") + ?.map(({ name }) => name) + .join("/")}`; + + const userAvailableEnvs = currentWorkspace?.environments?.filter( + ({ slug }) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath }) + ) || + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Folders, { environment: slug, secretPath }) + ) || + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment: slug, secretPath }) + ) + ); + // This is for dnd-kit. As react-query state mutation async // This will act as a placeholder to avoid a glitching animation on dropping items const [items, setItems] = useState< @@ -297,7 +313,6 @@ export const DashboardPage = () => { resolver: yupResolver(schema) }); - const { register, control, @@ -308,10 +323,35 @@ export const DashboardPage = () => { reset } = method; const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); - const isReadOnly = selectedEnv?.isWriteDenied; - const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; - const canDoRollback = !isReadOnly && !isAddOnly; - const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting; + + const isReadOnly = isFoldersLoading + ? true + : permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + const canDoRollback = !isReadOnly; + const isSubmitDisabled = + isReadOnly || + // not in rollback mode and no form has changed + (!isRollbackMode && !isDirty) || + // in rollback mode and don't have permission to do it + (isRollbackMode && + permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback)) || + isSubmitting; useEffect(() => { if (!isSnapshotChanging && Boolean(snapshotId)) { @@ -397,12 +437,12 @@ export const DashboardPage = () => { await performSecretRollback({ workspaceId, version: snapshotSecret.version, - environment: selectedEnvSlug, + environment, folderId }); setValue("isSnapshotMode", false); setSnaphotId(null); - queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug)); + queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, environment)); createNotification({ text: "Successfully rollback secrets", type: "success" @@ -428,18 +468,15 @@ export const DashboardPage = () => { } // just closing this if save is triggered from drawer handlePopUpClose("secretDetails"); - // when add only mode remove rest of things not created - const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec; // encrypt and format the secrets to batch api format // requests = [ {method:"", secret:""} ] const batchedSecret = transformSecretsToBatchSecretReq( deletedSecretIds.current, latestFileKey, - sec, + userSec, secrets?.secrets ); // type check - if (!selectedEnv?.slug) return; if (batchedSecret.length === 0) { reset(); return; @@ -449,7 +486,7 @@ export const DashboardPage = () => { requests: batchedSecret, workspaceId, folderId, - environment: selectedEnv?.slug + environment }); createNotification({ text: "Successfully saved changes", @@ -477,8 +514,7 @@ export const DashboardPage = () => { // eslint-disable-next-line no-alert if (!window.confirm(leaveConfirmDefaultMessage)) return; } - const env = wsEnv?.find((el) => el.slug === slug); - if (env) setSelectedEnv(env); + const query: Record = { ...router.query, env: slug }; delete query.folderId; router.push({ @@ -494,24 +530,29 @@ export const DashboardPage = () => { secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment }); }); }); - downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug); + downloadSecret(getValues("secrets"), secretsFromImport, environment); }; // record all deleted ids // This will make final deletion easier - const onSecretDelete = useCallback((index: number, secretName: string, id?: string, overrideId?: string) => { - if (id) deletedSecretIds.current.push({ - id, - secretName - }); - if (overrideId) deletedSecretIds.current.push({ - id: overrideId, - secretName - }); - remove(index); - // just the case if this is called from drawer - handlePopUpClose("secretDetails"); - }, []); + const onSecretDelete = useCallback( + (index: number, secretName: string, id?: string, overrideId?: string) => { + if (id) + deletedSecretIds.current.push({ + id, + secretName + }); + if (overrideId) + deletedSecretIds.current.push({ + id: overrideId, + secretName + }); + remove(index); + // just the case if this is called from drawer + handlePopUpClose("secretDetails"); + }, + [] + ); const onCreateWsTag = useCallback( async (tagName: string, tagColor: string) => { @@ -545,12 +586,12 @@ export const DashboardPage = () => { pathname: router.pathname, query: { id: workspaceId, - env: envQuery, + env: environment, folderId: id } }); }, - [envQuery, workspaceId] + [environment, workspaceId] ); const isEditFolder = Boolean(popUp?.folderForm?.data); @@ -560,7 +601,7 @@ export const DashboardPage = () => { try { await createFolder({ workspaceId, - environment: selectedEnv?.slug || "", + environment, folderName: name, parentFolderId: folderId }); @@ -585,7 +626,7 @@ export const DashboardPage = () => { await updateFolder({ folderId: id, workspaceId, - environment: selectedEnv?.slug || "", + environment, name }); createNotification({ @@ -601,7 +642,7 @@ export const DashboardPage = () => { }); } }, - [selectedEnv?.slug, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] + [environment, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] ); const handleFolderDelete = useCallback(async () => { @@ -609,7 +650,7 @@ export const DashboardPage = () => { try { deleteFolder({ workspaceId, - environment: selectedEnv?.slug || "", + environment, folderId: id }); createNotification({ @@ -624,18 +665,18 @@ export const DashboardPage = () => { type: "error" }); } - }, [selectedEnv?.slug, (popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); + }, [(popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); // SECRET IMPORT SECTION - const handleSecretImportCreate = async (env: string, secretPath: string) => { + const handleSecretImportCreate = async (env: string, secPath: string) => { try { await createSecretImport({ workspaceId, - environment: selectedEnv?.slug || "", + environment, folderId, secretImport: { environment: env, - secretPath + secretPath: secPath } }); createNotification({ @@ -659,7 +700,7 @@ export const DashboardPage = () => { if (secretImportCfg?._id) { await deleteSecretImport({ workspaceId, - environment: selectedEnvSlug, + environment, folderId, id: secretImportCfg?._id, secretImportEnv: importEnv, @@ -689,7 +730,7 @@ export const DashboardPage = () => { setItems(newImportOrder); updateSecretImportSync({ workspaceId, - environment: selectedEnvSlug, + environment, folderId, id: secretImportCfg?._id || "", secretImports: newImportOrder.map((el) => ({ @@ -734,7 +775,7 @@ export const DashboardPage = () => { const isSecretImportEmpty = !secretImportCfg?.imports?.length; const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty; - if (isSecretsLoading || isEnvListLoading) { + if (isSecretsLoading) { return (
loading animation @@ -742,10 +783,6 @@ export const DashboardPage = () => { ); } - const userAvailableEnvs = wsEnv?.filter( - ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied - ); - return (
@@ -753,7 +790,9 @@ export const DashboardPage = () => {
envir.slug === envQuery)[0].name || ""} + currentEnv={ + userAvailableEnvs?.filter((envir) => envir.slug === environment)?.[0]?.name || "" + } isFolderMode folders={folderData?.dir} isProjectRelated @@ -816,57 +855,83 @@ export const DashboardPage = () => {
-
- - handlePopUpOpen("secretSnapshots")} - > - - - -
-
- -
+ handlePopUpOpen("upgradePlan"); + }} + leftIcon={} + isLoading={isLoadingSnapshotCount} + isDisabled={!canDoRollback || !isAllowed} + className="h-10" + > + {snapshotCount} Commits + +
+ )} + {!isReadOnly && !isRollbackMode && (
- + {(isAllowed) => ( + + )} +
@@ -876,28 +941,45 @@ export const DashboardPage = () => {
- + {(isAllowed) => ( + + )} +
- + {(isAllowed) => ( + + )} +
@@ -917,17 +999,25 @@ export const DashboardPage = () => { Go back )} - + +
{ { searchTerm={searchFilter} /> - {fields.map(({ id, _id }, index) => ( - - ))} + {permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) ? ( + fields.map(({ id, _id }, index) => ( + + )) + ) : ( + + + + + + )} {!isReadOnly && !isRollbackMode && ( - + {(isAllowed) => ( + + )} + )} @@ -1014,6 +1131,8 @@ export const DashboardPage = () => { onSelectSnapshot={setSnaphotId} /> { onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)} /> - ; search?: string; + environment: string; + secretPath: string; onFolderUpdate: (folderId: string, name: string) => void; onFolderDelete: (folderId: string, name: string) => void; onFolderOpen: (folderId: string) => void; @@ -18,7 +23,9 @@ export const FolderSection = memo( onFolderDelete: handleFolderDelete, onFolderOpen: handleFolderOpen, search = "", - folders = [] + folders = [], + environment, + secretPath }: Props) => { return ( <> @@ -47,32 +54,48 @@ export const FolderSection = memo( {name}
-
- - handleFolderUpdate(id, name)} - ariaLabel="expand" - > - - - -
-
- - handleFolderDelete(id, name)} - > - - - -
+ + {(isAllowed) => ( +
+ + handleFolderUpdate(id, name)} + ariaLabel="expand" + > + + + +
+ )} +
+ + {(isAllowed) => ( +
+ + handleFolderDelete(id, name)} + > + + + +
+ )} +
diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx index 0f8446fcc..ef85c4896 100644 --- a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx +++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx @@ -1,7 +1,9 @@ import { useFormContext, useWatch } from "react-hook-form"; +import { subject } from "@casl/ability"; import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, Drawer, @@ -14,6 +16,7 @@ import { Switch, TextArea } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; @@ -21,6 +24,8 @@ import { GenRandomNumber } from "./GenRandomNumber"; type Props = { isDrawerOpen: boolean; + environment: string; + secretPath: string; onOpenChange: (isOpen: boolean) => void; index: number; isReadOnly?: boolean; @@ -39,7 +44,9 @@ export const SecretDetailDrawer = ({ isReadOnly, onSecretDelete, onSave, - onEnvCompare + onEnvCompare, + environment, + secretPath }: Props): JSX.Element => { const [canRevealSecVal, setCanRevealSecVal] = useToggle(); const [canRevealSecOverride, setCanRevealSecOverride] = useToggle(); @@ -85,20 +92,34 @@ export const SecretDetailDrawer = ({
- - + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} +
} diff --git a/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx index 1c992df25..df022f4bb 100644 --- a/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -1,6 +1,7 @@ import { ChangeEvent, DragEvent, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { subject } from "@casl/ability"; import { faSquareCheck } from "@fortawesome/free-regular-svg-icons"; import { faClone, @@ -15,6 +16,7 @@ import { twMerge } from "tailwind-merge"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; // TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality import { parseDotEnv } from "@app/components/utilities/parseDotEnv"; import { @@ -32,6 +34,7 @@ import { Skeleton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useDebounce, usePopUp, useToggle } from "@app/hooks"; import { useGetProjectSecrets } from "@app/hooks/api"; import { UserWsKeyPair } from "@app/hooks/api/types"; @@ -76,6 +79,8 @@ type Props = { environments?: { name: string; slug: string }[]; workspaceId: string; decryptFileKey: UserWsKeyPair; + environment: string; + secretPath: string; }; export const SecretDropzone = ({ @@ -84,7 +89,9 @@ export const SecretDropzone = ({ onAddNewSecret, environments = [], workspaceId, - decryptFileKey + decryptFileKey, + environment, + secretPath }: Props): JSX.Element => { const { t } = useTranslation(); const [isDragActive, setDragActive] = useToggle(); @@ -107,16 +114,16 @@ export const SecretDropzone = ({ defaultValues: { secretPath: "/", environment: environments?.[0]?.slug } }); - const secretPath = watch("secretPath"); + const envCopySecPath = watch("secretPath"); const selectedEnvSlug = watch("environment"); - const debouncedSecretPath = useDebounce(secretPath); + const debouncedEnvCopySecretPath = useDebounce(envCopySecPath); const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ workspaceId, env: selectedEnvSlug, - secretPath: debouncedSecretPath, + secretPath: debouncedEnvCopySecretPath, isPaused: - !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) && + !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath)) && !popUp.importSecEnv.isOpen, decryptFileKey }); @@ -124,7 +131,7 @@ export const SecretDropzone = ({ useEffect(() => { setValue("secrets", {}); setSearchFilter(""); - }, [debouncedSecretPath]); + }, [debouncedEnvCopySecretPath]); const handleDrag = (e: DragEvent) => { e.preventDefault(); @@ -175,7 +182,7 @@ export const SecretDropzone = ({ e.dataTransfer.dropEffect = "copy"; setDragActive.off(); - parseFile(e.dataTransfer.files[0], e.dataTransfer?.files?.[0]?.type === "application/json"); + parseFile(e.dataTransfer.files[0]); }; const handleFileUpload = (e: ChangeEvent) => { @@ -234,13 +241,21 @@ export const SecretDropzone = ({

{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}

- + + {(isAllowed) => ( + + )} +
- +
+ + {(isAllowed) => ( + + )} + +
{!isSmaller && ( - + + {(isAllowed) => ( + + )} + )}
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx index 31752aa02..6d9f23e47 100644 --- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx +++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { subject } from "@casl/ability"; import { useSortable } from "@dnd-kit/sortable"; import { faFileImport, @@ -9,12 +10,15 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { useToggle } from "@app/hooks/useToggle"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; type Props = { onDelete: (environment: string, secretPath: string) => void; + environment: string; + secretPath: string; importedEnv: string; importedSecPath: string; importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[]; @@ -39,7 +43,9 @@ export const SecretImportItem = ({ importedSecPath, onDelete, importedSecrets = [], - searchTerm = "" + searchTerm = "", + secretPath, + environment }: Props) => { const [isExpanded, setIsExpanded] = useToggle(); const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({ @@ -49,7 +55,9 @@ export const SecretImportItem = ({ const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv); useEffect(() => { - const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())) + const filteredSecrets = importedSecrets.filter((secret) => + secret.key.toUpperCase().includes(searchTerm.toUpperCase()) + ); if (filteredSecrets.length > 0 && searchTerm) { setIsExpanded.on(); @@ -58,7 +66,6 @@ export const SecretImportItem = ({ } }, [searchTerm]); - useEffect(() => { if (isDragging) { setIsExpanded.off(); @@ -78,7 +85,11 @@ export const SecretImportItem = ({ className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700" onClick={() => setIsExpanded.toggle()} > - + @@ -106,28 +117,39 @@ export const SecretImportItem = ({
-
- - { - evt.stopPropagation(); - onDelete(importedEnv, importedSecPath); - }} - > - - - -
+ + {(isAllowed) => ( +
+ + { + evt.stopPropagation(); + onDelete(importedEnv, importedSecPath); + }} + > + + + +
+ )} +
{isExpanded && !isDragging && ( - +
@@ -146,19 +168,26 @@ export const SecretImportItem = ({ )} - {importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => ( - - - - - - ))} + {importedSecrets + .filter((secret) => + secret.key.toUpperCase().includes(searchTerm.toUpperCase()) + ) + .map(({ key, value, overriden }, index) => ( + + + + + + ))}
- {key} - - - - -
+ {key} + + + + +
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx index 9d796d079..db12e84a4 100644 --- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx +++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx @@ -59,13 +59,23 @@ export const computeImportedSecretRows = ( type Props = { secrets?: DecryptedSecret[]; importedSecrets?: TImportedSecrets; + environment: string; + secretPath: string; onSecretImportDelete: (env: string, secPath: string) => void; items: { id: string; environment: string; secretPath: string }[]; searchTerm: string; }; export const SecretImportSection = memo( - ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => { + ({ + secrets = [], + environment, + secretPath, + importedSecrets = [], + onSecretImportDelete, + items = [], + searchTerm = "" + }: Props) => { const { currentWorkspace } = useWorkspace(); const environments = currentWorkspace?.environments || []; @@ -82,6 +92,8 @@ export const SecretImportSection = memo( secrets, environments )} + secretPath={secretPath} + environment={environment} onDelete={onSecretImportDelete} importedSecPath={impSecPath} searchTerm={searchTerm} diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 6c089d695..c31c6df2e 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -8,6 +8,7 @@ import { UseFormSetValue, useWatch } from "react-hook-form"; +import { subject } from "@casl/ability"; import { faCheck, faCodeBranch, @@ -22,26 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cx } from "cva"; import { twMerge } from "tailwind-merge"; +// TODO:(akhilmhdh): Refactor this +import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { + FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, + PopoverContent, PopoverTrigger, SecretInput, Tag, + TextArea, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; -import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; type Props = { index: number; + environment: string; + secretPath: string; // backend generated unique id secUniqId?: string; // permission and external state's that decided to hide or show @@ -69,6 +78,8 @@ type Props = { export const SecretInputRow = memo( ({ index, + secretPath, + environment, isSecretValueHidden, onRowExpand, isReadOnly, @@ -79,7 +90,7 @@ export const SecretInputRow = memo( onSecretDelete, searchTerm, control, - // register, + register, setValue, isKeyError, keyError, @@ -217,7 +228,6 @@ export const SecretInputRow = memo(
{index + 1}
-
- - - - - + + {(isAllowed) => ( + + + + )} +
{!isAddOnly && (
- - -
- -
-
-
+ + {(isAllowed) => ( + +
+ +
+
+ )} +
)} - -
- - - + + +
+ - - - - onSelectTag(wsTag)} - handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} - handleTagOnMouseLeave={() => handleTagOnMouseLeave()} - checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} - handleOnCreateTagOpen={() => onCreateTagOpen()} - /> - -
- + {(isAllowed) => ( + + + + )} + +
+ + + +