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/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index db2958b32..4961ef274 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -34,7 +34,7 @@ The changelog below reflects new product developments and updates on a monthly b - Released the [Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform#5-run-terraform). - Updated the usage and billing page. Added the free trial for the professional tier. -- Added native intergations with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages). +- Added native integrations with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages). - Completed a penetration test with a `very good` result. - Added support for multi-line secrets. @@ -70,7 +70,7 @@ The changelog below reflects new product developments and updates on a monthly b ## Feb 2023 - Upgraded private key encryption/decryption mechanism to use Argon2id and 256-bit protected keys. -- Added preliminary emai-based 2FA capability. +- Added preliminary email-based 2FA capability. - Added suspicious login alerting if user logs in via new device or IP address. - Added documentation for PM2 integration. - Added secret backups support for the CLI; it now fetches and caches secrets locally to be used in the event of future failed fetch. @@ -120,7 +120,7 @@ The changelog below reflects new product developments and updates on a monthly b ## Sep 2022 - Added capability to change user roles in projects. -- Added capabilty to delete projects. +- Added capability to delete projects. - Added Stripe. - Added default environments (development, staging, production) for new users with example key-pairs. - Added loading indicators. diff --git a/docs/cli/token.mdx b/docs/cli/token.mdx index c6cc6dcdd..b5a8dc6a9 100644 --- a/docs/cli/token.mdx +++ b/docs/cli/token.mdx @@ -1,11 +1,11 @@ --- title: "Infisical Token" -description: "How to use Infical service token within the CLI." +description: "How to use Infisical service token within the CLI." --- Prerequisite: [Infisical Token and How to Generate One](/documentation/platform/token). -It's possible to use the CLI to sync environment varialbes without manually entering login credentials by using a service token in the prerequisite link above. +It's possible to use the CLI to sync environment variables without manually entering login credentials by using a service token in the prerequisite link above. ## Feeding Infisical Token to the CLI diff --git a/docs/contributing/pull-requests.mdx b/docs/contributing/pull-requests.mdx index a06d9f164..15b95926c 100644 --- a/docs/contributing/pull-requests.mdx +++ b/docs/contributing/pull-requests.mdx @@ -29,7 +29,7 @@ Feel free to add a short video or screenshots of what your PR achieves. ## Getting your PR reviewed -Once your PR is reviewed, one or two relevent members of the Infisical team should review and approve the PR before it is merged. You should coordinate and ping the team member closest to the submitted functionality via our [Slack](https://infisical.com/slack) to review your PR. +Once your PR is reviewed, one or two relevant members of the Infisical team should review and approve the PR before it is merged. You should coordinate and ping the team member closest to the submitted functionality via our [Slack](https://infisical.com/slack) to review your PR. - Vlad: Frontend, Web UI - Tony: Backend, SDKs, Security @@ -44,4 +44,4 @@ Once everything is good, the team member(s) will approve the PR to be merged int reviewing PRs once they are fully complete and well-tested. In the past, we've often had to review low-quality PRs up to 10 times which severely restricts our capacity to address other issues, PRs, and initiatives in the pipeline. As such, we ask of the community to submit higher-quality PRs that, for example, don't break existing code; in return we'll prioritize the first 3 reviews for PRs. - \ No newline at end of file + diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx index 065835fb3..38a677ab0 100644 --- a/docs/documentation/platform/project.mdx +++ b/docs/documentation/platform/project.mdx @@ -27,10 +27,10 @@ In most cases, environment variables belong to specific environments: developmen ### Personal overrides -Every environment variable value can be overriden with a custom value. +Every environment variable value can be overridden with a custom value. -- An overriden value can only be read and accesssed by the user that overrode the original shared value. -- A (default) shared value can be read and accesssed by other users in a project. +- An overridden value can only be read and accessed by the user that overrode the original shared value. +- A (default) shared value can be read and accessed by other users in a project. You can turn overrides on/off by toggling the override/branch icon: diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 0f50bf639..d03f57128 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -5,7 +5,7 @@ description: "Configure your environment variables when self-hosting Infisical." ## Backend environment variables -Depending on your choosen self hosted deployment method, you may need to configured at least the required environment variable listed below. +Depending on your chosen self hosted deployment method, you may need to configured at least the required environment variable listed below. Other environment variables are listed below to increase the functionality of your self hosted instance based on your use case. @@ -159,7 +159,7 @@ Other environment variables are listed below to increase the functionality of yo #### JWT - JWT token lifetime expressed in seconds or a string describing a time span + JWT token lifetime expressed in seconds or a string describing a time span diff --git a/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx b/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx index 8049cd3e5..f1e739f08 100644 --- a/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx +++ b/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx @@ -3,10 +3,10 @@ title: "Digital Ocean" description: "Learn to install Infisical on Digital Ocean" --- -Infisical can be deployed on a Kubernetes cluster with a single click through our Digital Ocean marketplace application. -The initiation of the installation process triggers the creation of a Kubernetes cluster, followed by the installation of Infisical onto that cluster. +Infisical can be deployed on a Kubernetes cluster with a single click through our Digital Ocean marketplace application. +The initiation of the installation process triggers the creation of a Kubernetes cluster, followed by the installation of Infisical onto that cluster. -This automated deploymnet method uses the same proccess under the hood as the manual [Kubernetes installation guide](./kubernetes-helm). +This automated deployment method uses the same process under the hood as the manual [Kubernetes installation guide](./kubernetes-helm). ### Initiate the installation @@ -23,5 +23,5 @@ Within this section, you'll find the newly created load balancer for Infisical. ### Adjusting configurations If you need to either upgrade or downgrade Infisical, or modify environment variables to alter its functionality, refer to our [Kubernetes installation](./kubernetes-helm) page for detailed instructions. -Because Digital Ocean deploys the same Helm application as described in our [Kubernetes installation](./kubernetes-helm) guide, you can utilize that guide to implement the required changes. -It's important to note that any modifications requires familiarly with Helm package manager. \ No newline at end of file +Because Digital Ocean deploys the same Helm application as described in our [Kubernetes installation](./kubernetes-helm) guide, you can utilize that guide to implement the required changes. +It's important to note that any modifications requires familiarly with Helm package manager. diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 83f3c8f6e..37383fa82 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -43,14 +43,14 @@ frontend: replicaCount: 2 image: repository: infisical/frontend - tag: "v0.26.0" # <--- frontend version + tag: "v0.34.2" # <--- frontend version pullPolicy: Always backend: replicaCount: 2 image: repository: infisical/backend - tag: "v0.26.0" # <--- backend version + tag: "v0.34.2" # <--- backend version pullPolicy: Always ``` @@ -111,14 +111,14 @@ frontend: replicaCount: 2 image: repository: infisical/frontend - tag: "v0.26.0" # <--- frontend version + tag: "v0.34.2" # <--- frontend version pullPolicy: Always backend: replicaCount: 2 image: repository: infisical/backend - tag: "v0.26.0" # <--- backend version + tag: "v0.34.2" # <--- backend version pullPolicy: Always backendEnvironmentVariables: @@ -144,7 +144,7 @@ ingress: replicaCount: 4 image: repository: infisical/frontend - tag: "v0.1.3" + tag: "v0.34.2" # <--- frontend version pullPolicy: IfNotPresent kubeSecretRef: null service: @@ -160,7 +160,7 @@ ingress: replicaCount: 4 image: repository: infisical/backend - tag: "v0.1.3" + tag: "v0.34.2" # <--- backend version pullPolicy: IfNotPresent kubeSecretRef: null service: 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) => ( + + )} +
@@ -734,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 332557732..45a1cbe71 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -12,13 +12,14 @@ import { QueryClientProvider } from "@tanstack/react-query"; import NProgress from "nprogress"; import NotificationProvider from "@app/components/context/Notifications/NotificationProvider"; -import { IntercomProvider } from "@app/components/utilities/intercom/intercomProvider"; import Telemetry from "@app/components/utilities/telemetry/Telemetry"; import { TooltipProvider } from "@app/components/v2"; import { publicPaths } from "@app/const"; import { AuthProvider, + OrgPermissionProvider, OrgProvider, + ProjectPermissionProvider, SubscriptionProvider, UserProvider, WorkspaceProvider @@ -47,7 +48,6 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => const telemetry = new Telemetry().getInstance(); const handleRouteChange = () => { - // (window).Intercom('update'); if (typeof window !== "undefined") { telemetry.capture("$pageview"); } @@ -97,19 +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) => ( + + + + )} + +
+ + + +