diff --git a/backend/src/controllers/v1/botController.ts b/backend/src/controllers/v1/botController.ts index 65251e83a..3a2ff606a 100644 --- a/backend/src/controllers/v1/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -7,7 +7,7 @@ import * as reqValidator from "../../validation/bot"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { BadRequestError } from "../../utils/errors"; @@ -28,7 +28,11 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => { const { params: { workspaceId } } = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -70,7 +74,11 @@ export const setBotActiveState = async (req: Request, res: Response) => { } const userId = req.user._id; - const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString()); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: bot.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index ec166df48..d04ead63f 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -25,7 +25,7 @@ import * as reqValidator from "../../validation/integrationAuth"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { getIntegrationAuthAccessHelper } from "../../helpers"; @@ -40,15 +40,15 @@ export const getIntegrationAuth = async (req: Request, res: Response) => { const integrationAuth = await IntegrationAuth.findById(integrationAuthId); - if (!integrationAuth) - return res.status(400).send({ - message: "Failed to find integration authorization" - }); + if (!integrationAuth) return res.status(400).send({ + message: "Failed to find integration authorization" + }); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -79,7 +79,11 @@ export const oAuthExchange = async (req: Request, res: Response) => { } = await validateRequest(reqValidator.OauthExchangeV1, req); if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration"); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Integrations @@ -131,7 +135,11 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken } } = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Integrations @@ -224,11 +232,12 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { const { integrationAuth, accessToken, accessId } = await getIntegrationAuthAccessHelper({ integrationAuthId: new Types.ObjectId(integrationAuthId) }); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -263,10 +272,11 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -299,10 +309,11 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -360,10 +371,11 @@ export const getIntegrationAuthChecklyGroups = async (req: Request, res: Respons integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -413,10 +425,11 @@ export const getIntegrationAuthQoveryOrgs = async (req: Request, res: Response) integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -465,10 +478,11 @@ export const getIntegrationAuthQoveryProjects = async (req: Request, res: Respon integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -526,10 +540,11 @@ export const getIntegrationAuthQoveryEnvironments = async (req: Request, res: Re integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -587,10 +602,11 @@ export const getIntegrationAuthQoveryApps = async (req: Request, res: Response) integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -648,10 +664,11 @@ export const getIntegrationAuthQoveryContainers = async (req: Request, res: Resp integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -709,10 +726,11 @@ export const getIntegrationAuthQoveryJobs = async (req: Request, res: Response) integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -771,10 +789,11 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -864,10 +883,11 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -988,10 +1008,11 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -1044,10 +1065,11 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -1132,10 +1154,11 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -1201,10 +1224,11 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => { integrationAuthId: new Types.ObjectId(integrationAuthId) }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index e709d9d0c..d426be0a1 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -13,7 +13,7 @@ import * as reqValidator from "../../validation/integration"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; @@ -51,11 +51,12 @@ export const createIntegration = async (req: Request, res: Response) => { ); if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" }); - - const { permission } = await getUserProjectPermissions( - req.user._id, - integrationAuth.workspace._id.toString() - ); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integrationAuth.workspace._id + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Integrations @@ -164,10 +165,11 @@ export const updateIntegration = async (req: Request, res: Response) => { const integration = await Integration.findById(integrationId); if (!integration) throw BadRequestError({ message: "Integration not found" }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integration.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integration.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations @@ -234,10 +236,11 @@ export const deleteIntegration = async (req: Request, res: Response) => { const integration = await Integration.findById(integrationId); if (!integration) throw BadRequestError({ message: "Integration not found" }); - const { permission } = await getUserProjectPermissions( - req.user._id, - integration.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: integration.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations @@ -285,7 +288,11 @@ export const manualSync = async (req: Request, res: Response) => { body: { workspaceId, environment } } = await validateRequest(reqValidator.ManualSyncV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations diff --git a/backend/src/controllers/v1/keyController.ts b/backend/src/controllers/v1/keyController.ts index 242cb8f82..956487814 100644 --- a/backend/src/controllers/v1/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -9,7 +9,7 @@ import * as reqValidator from "../../validation/key"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; @@ -26,7 +26,11 @@ export const uploadKey = async (req: Request, res: Response) => { body: { key } } = await validateRequest(reqValidator.UploadKeyV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Member diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 928d5b50b..cb11c4b74 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -12,7 +12,7 @@ import * as reqValidator from "../../validation/membership"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { BadRequestError } from "../../utils/errors"; @@ -63,11 +63,12 @@ export const deleteMembership = async (req: Request, res: Response) => { if (!membershipToDelete) { throw new Error("Failed to delete workspace membership that doesn't exist"); } - - const { permission } = await getUserProjectPermissions( - req.user._id, - membershipToDelete.workspace.toString() - ); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: membershipToDelete.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Member @@ -118,10 +119,11 @@ export const changeMembershipRole = async (req: Request, res: Response) => { throw new Error("Failed to find membership to change role"); } - const { permission } = await getUserProjectPermissions( - req.user._id, - membershipToChangeRole.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: membershipToChangeRole.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Member @@ -191,7 +193,12 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { params: { workspaceId }, body: { email } } = await validateRequest(InviteUserToWorkspaceV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Member diff --git a/backend/src/controllers/v1/secretImpsController.ts b/backend/src/controllers/v1/secretImpsController.ts index 80adfc69a..14c99bb03 100644 --- a/backend/src/controllers/v1/secretImpsController.ts +++ b/backend/src/controllers/v1/secretImpsController.ts @@ -1,4 +1,6 @@ + import { Request, Response } from "express"; +import { Types } from "mongoose"; import { isValidScope } from "../../helpers"; import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models"; import { getAllImportedSecrets } from "../../services/SecretImportService"; @@ -15,7 +17,7 @@ import * as reqValidator from "../../validation/secretImports"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError, subject } from "@casl/ability"; @@ -105,7 +107,11 @@ export const createSecretImp = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } else { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory }) @@ -313,10 +319,11 @@ export const updateSecretImport = async (req: Request, res: Response) => { } } else { // non token entry check - const { permission } = await getUserProjectPermissions( - req.user._id, - importSecDoc.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: importSecDoc.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { @@ -442,10 +449,11 @@ export const deleteSecretImport = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } else { - const { permission } = await getUserProjectPermissions( - req.user._id, - importSecDoc.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: importSecDoc.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { @@ -550,7 +558,11 @@ export const getSecretImports = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } else { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { @@ -604,7 +616,11 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } else { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { @@ -657,10 +673,11 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => { permissionCheckFn = (env: string, secPath: string) => isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath); } else { - const { permission } = await getUserProjectPermissions( - req.user._id, - importSecDoc.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: importSecDoc.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index af9ee5277..52627b2e3 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -17,7 +17,7 @@ import { import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import * as reqValidator from "../../validation/folders"; @@ -125,7 +125,11 @@ export const createFolder = async (req: Request, res: Response) => { } } else { // user check - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory }) @@ -332,7 +336,11 @@ export const updateFolderById = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } else { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory }) @@ -502,7 +510,11 @@ export const deleteFolder = async (req: Request, res: Response) => { } } else { // check that user is a member of the workspace - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory }) @@ -649,7 +661,10 @@ export const getFolders = async (req: Request, res: Response) => { } } else { // check that user is a member of the workspace - await getUserProjectPermissions(req.user._id, workspaceId); + await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); } const folders = await Folder.findOne({ workspace: workspaceId, environment }); diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts index 09175ebfe..852a3aeed 100644 --- a/backend/src/controllers/v1/webhookController.ts +++ b/backend/src/controllers/v1/webhookController.ts @@ -16,7 +16,7 @@ import * as reqValidator from "../../validation/webhooks"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; @@ -26,7 +26,11 @@ export const createWebhook = async (req: Request, res: Response) => { body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } } = await validateRequest(reqValidator.CreateWebhookV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks @@ -98,11 +102,11 @@ export const updateWebhook = async (req: Request, res: Response) => { if (!webhook) { throw BadRequestError({ message: "Webhook not found!!" }); } - - const { permission } = await getUserProjectPermissions( - req.user._id, - webhook.workspace.toString() - ); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: webhook.workspace + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks @@ -146,10 +150,11 @@ export const deleteWebhook = async (req: Request, res: Response) => { throw ResourceNotFoundError({ message: "Webhook not found!!" }); } - const { permission } = await getUserProjectPermissions( - req.user._id, - webhook.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: webhook.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks @@ -193,10 +198,11 @@ export const testWebhook = async (req: Request, res: Response) => { throw BadRequestError({ message: "Webhook not found!!" }); } - const { permission } = await getUserProjectPermissions( - req.user._id, - webhook.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: webhook.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks @@ -236,8 +242,12 @@ export const listWebhooks = async (req: Request, res: Response) => { const { query: { environment, workspaceId, secretPath } } = await validateRequest(reqValidator.ListWebhooksV1, req); - - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 1b3a9fd3f..9d3ae5b80 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -25,7 +25,7 @@ import * as reqValidator from "../../validation"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; /** @@ -39,7 +39,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Member @@ -72,7 +76,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Member @@ -195,7 +203,11 @@ export const deleteWorkspace = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.DeleteWorkspaceV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace @@ -223,7 +235,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => { body: { name } } = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace @@ -257,7 +273,12 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { const { params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -283,7 +304,11 @@ export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: R params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Integrations @@ -309,7 +334,11 @@ export const getWorkspaceServiceTokens = async (req: Request, res: Response) => params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 130e2e225..3b412638b 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -12,14 +12,12 @@ import { 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 + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { SecretImport } from "../../models"; @@ -114,7 +112,11 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) => body: { environmentName, environmentSlug } } = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Environments @@ -191,7 +193,11 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response) body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName } } = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Environments @@ -322,7 +328,11 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) => body: { environmentName, environmentSlug, oldEnvironmentSlug } } = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Environments @@ -511,7 +521,11 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => body: { environmentSlug } } = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Environments @@ -587,98 +601,4 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => workspace: workspaceId, environment: environmentSlug }); -}; - -// TODO(akhilmhdh) after rbac this can be completely removed -export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => { - /* - #swagger.summary = 'Get all accessible environments of a workspace' - #swagger.description = 'Fetch all environments that the user has access to in a specified workspace' - - #swagger.security = [{ - "apiKeyAuth": [] - }] - - #swagger.parameters['workspaceId'] = { - "description": "ID of the workspace", - "required": true, - "type": "string", - "in": "path" - } - - #swagger.responses[200] = { - content: { - "application/json": { - "schema": { - "type": "object", - "properties": { - "accessibleEnvironments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "Development" - }, - "slug": { - "type": "string", - "example": "development" - }, - "isWriteDenied": { - "type": "boolean", - "example": false - }, - "isReadDenied": { - "type": "boolean", - "example": false - } - } - } - } - }, - "description": "List of environments the user has access to in the specified workspace" - } - } - } - } - */ - const { - params: { workspaceId } - } = await validateRequest(reqValidator.GetAllAccessibileEnvironmentsOfWorkspaceV2, req); - - const { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions( - req.user._id, - workspaceId - ); - - const accessibleEnvironments: any = []; - const deniedPermission = workspacesUserIsMemberOf.deniedPermissions; - - const relatedWorkspace = await Workspace.findById(workspaceId); - if (!relatedWorkspace) { - 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 - }); - if (isReadBlocked && isWriteBlocked) { - return; - } else { - accessibleEnvironments.push({ - name: environment.name, - slug: environment.slug, - isWriteDenied: isWriteBlocked, - isReadDenied: isReadBlocked - }); - } - }); - - res.json({ accessibleEnvironments }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/v2/membershipController.ts b/backend/src/controllers/v2/membershipController.ts index 537134963..d6d25dad5 100644 --- a/backend/src/controllers/v2/membershipController.ts +++ b/backend/src/controllers/v2/membershipController.ts @@ -8,7 +8,7 @@ import { EEAuditLogService } from "../../ee/services"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { sendMail } from "../../helpers"; import { validateRequest } from "../../helpers/validation"; @@ -27,7 +27,11 @@ export const addUserToWorkspace = async (req: Request, res: Response) => { if (!workspace) throw new Error("Failed to find workspace"); // check permission - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Member diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index b0362f6a4..362221bf4 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -39,7 +39,7 @@ import { import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError, subject } from "@casl/ability"; @@ -159,7 +159,11 @@ export const batchSecrets = async (req: Request, res: Response) => { } // not using service token using auth if (!(req.authData.authPayload instanceof ServiceTokenData)) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + if (createSecrets.length) ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index b8f1b8f43..b49639d3f 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -11,7 +11,7 @@ import * as reqValidator from "../../validation/serviceTokenData"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { Types } from "mongoose"; @@ -75,7 +75,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const { body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv } } = await validateRequest(reqValidator.CreateServiceTokenV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens @@ -151,10 +156,11 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { 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() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: serviceTokenData.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 668903222..c803b0e18 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -7,7 +7,7 @@ import { validateRequest } from "../../helpers/validation"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import * as reqValidator from "../../validation/tags"; @@ -17,7 +17,11 @@ export const createWorkspaceTag = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Tags @@ -45,10 +49,11 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => { throw BadRequestError(); } - const { permission } = await getUserProjectPermissions( - req.user._id, - tagFromDB.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: tagFromDB.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Tags @@ -66,7 +71,12 @@ export const getWorkspaceTags = async (req: Request, res: Response) => { const { params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceTagsV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Tags diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 040c68f31..8c5e70952 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -16,7 +16,7 @@ import * as reqValidator from "../../validation"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; @@ -272,7 +272,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.Member @@ -352,7 +356,11 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) => body: { role } } = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Member @@ -420,7 +428,11 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => params: { workspaceId, membershipId } } = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.Member @@ -452,7 +464,11 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => { body: { autoCapitalization } } = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.Settings diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 1cc3f1953..e1e54d4d0 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -3,11 +3,16 @@ import { Types } from "mongoose"; import { EventService, SecretService } from "../../services"; import { eventPushSecrets } from "../../events"; import { BotService } from "../../services"; -import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets"; +import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { getAllImportedSecrets } from "../../services/SecretImportService"; -import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models"; -import { Permission } from "../../models/serviceTokenDataV3"; +import { + Folder, + IServiceTokenData, + Membership, + ServiceTokenData, + User +} from "../../models"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; import { validateRequest } from "../../helpers/validation"; @@ -15,13 +20,10 @@ import * as reqValidator from "../../validation/secrets"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError, subject } from "@casl/ability"; -import { - validateServiceTokenDataClientForWorkspace, - validateServiceTokenDataV3ClientForWorkspace -} from "../../validation"; +import { validateServiceTokenDataClientForWorkspace } from "../../validation"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; import { ActorType } from "../../ee/models"; import { UnauthorizedRequestError } from "../../utils/errors"; @@ -31,7 +33,6 @@ import { getSecretPolicyOfBoard } from "../../ee/services/SecretApprovalService"; import { CommitType } from "../../ee/models/secretApprovalRequest"; -import { IRole } from "../../ee/models/role"; const checkSecretsPermission = async ({ authData, @@ -47,36 +48,31 @@ const checkSecretsPermission = async ({ secretAction: ProjectPermissionActions; // CRUD }): Promise<{ authVerifier: (env: string, secPath: string) => boolean; - membership?: Omit & { customRole: IRole }; }> => { let STV2RequiredPermissions = []; - let STV3RequiredPermissions: Permission[] = []; switch (secretAction) { case ProjectPermissionActions.Create: STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS]; - STV3RequiredPermissions = [Permission.WRITE]; break; case ProjectPermissionActions.Read: STV2RequiredPermissions = [PERMISSION_READ_SECRETS]; - STV3RequiredPermissions = [Permission.READ]; break; case ProjectPermissionActions.Edit: STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS]; - STV3RequiredPermissions = [Permission.WRITE]; break; case ProjectPermissionActions.Delete: STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS]; - STV3RequiredPermissions = [Permission.WRITE]; break; } switch (authData.actor.type) { case ActorType.USER: { - const { permission, membership } = await getUserProjectPermissions( - authData.actor.metadata.userId, - workspaceId - ); + const { permission } = await getAuthDataProjectPermissions({ + authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( secretAction, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) @@ -89,8 +85,7 @@ const checkSecretsPermission = async ({ environment: env, secretPath: secPath }) - ), - membership + ) }; } case ActorType.SERVICE: { @@ -104,22 +99,24 @@ const checkSecretsPermission = async ({ return { authVerifier: () => true }; } case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ + const { permission } = await getAuthDataProjectPermissions({ authData, - serviceTokenData: authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: STV3RequiredPermissions + workspaceId: new Types.ObjectId(workspaceId) }); + + ForbiddenError.from(permission).throwUnlessCan( + secretAction, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); return { authVerifier: (env: string, secPath: string) => - isValidScopeV3({ - authPayload: authData.authPayload as IServiceTokenDataV3, - environment: env, - secretPath: secPath, - requiredPermissions: STV3RequiredPermissions - }) + permission.can( + secretAction, + subject(ProjectPermissionSub.Secrets, { + environment: env, + secretPath: secPath + }) + ) }; } default: { @@ -199,17 +196,20 @@ export const getSecretsRaw = async (req: Request, res: Response) => { query: { include_imports: includeImports } } = validatedData; - // 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) - ) { - const scope = serviceTokenDetails.scopes[0]; - secretPath = scope.secretPath; - environment = scope.environment; - workspaceId = serviceTokenDetails.workspace.toString(); + if (req.authData.authPayload instanceof ServiceTokenData) { + + // 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) + ) { + const scope = serviceTokenDetails.scopes[0]; + secretPath = scope.secretPath; + environment = scope.environment; + workspaceId = serviceTokenDetails.workspace.toString(); + } } if (!environment || !workspaceId) @@ -900,7 +900,7 @@ export const createSecret = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.CreateSecretV3, req); - const { membership } = await checkSecretsPermission({ + await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -908,35 +908,42 @@ export const createSecret = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Create }); - if (membership && type !== "personal") { - const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); - if (secretApprovalPolicy) { - const secretApprovalRequest = await generateSecretApprovalRequest({ - workspaceId, - environment, - secretPath, - policy: secretApprovalPolicy, - commiterMembershipId: membership._id.toString(), - authData: req.authData, - data: { - [CommitType.CREATE]: [ - { - secretName, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentIV, - secretCommentTag, - secretCommentCiphertext, - skipMultilineEncoding, - secretKeyTag, - secretKeyCiphertext, - secretKeyIV - } - ] - } - }); - return res.send({ approval: secretApprovalRequest }); + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (membership && type !== "personal") { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + authData: req.authData, + data: { + [CommitType.CREATE]: [ + { + secretName, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretKeyTag, + secretKeyCiphertext, + secretKeyIV + } + ] + } + }); + return res.send({ approval: secretApprovalRequest }); + } } } @@ -1009,7 +1016,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { throw BadRequestError({ message: "Missing encrypted key" }); } - const { membership } = await checkSecretsPermission({ + await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -1017,37 +1024,44 @@ export const updateSecretByName = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Edit }); - if (membership && type !== "personal") { - const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); - if (secretApprovalPolicy) { - const secretApprovalRequest = await generateSecretApprovalRequest({ - workspaceId, - environment, - secretPath, - policy: secretApprovalPolicy, - commiterMembershipId: membership._id.toString(), - authData: req.authData, - data: { - [CommitType.UPDATE]: [ - { - secretName, - newSecretName, - secretValueCiphertext, - secretValueIV, - secretValueTag, - tags, - secretCommentIV, - secretCommentTag, - secretCommentCiphertext, - skipMultilineEncoding, - secretKeyTag, - secretKeyCiphertext, - secretKeyIV - } - ] - } - }); - return res.send({ approval: secretApprovalRequest }); + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (membership && type !== "personal") { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + authData: req.authData, + data: { + [CommitType.UPDATE]: [ + { + secretName, + newSecretName, + secretValueCiphertext, + secretValueIV, + secretValueTag, + tags, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretKeyTag, + secretKeyCiphertext, + secretKeyIV + } + ] + } + }); + return res.send({ approval: secretApprovalRequest }); + } } } @@ -1097,7 +1111,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.DeleteSecretByNameV3, req); - const { membership } = await checkSecretsPermission({ + await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -1105,25 +1119,32 @@ export const deleteSecretByName = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Delete }); - if (membership && type !== "personal") { - const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); - if (secretApprovalPolicy) { - const secretApprovalRequest = await generateSecretApprovalRequest({ - workspaceId, - environment, - secretPath, - authData: req.authData, - policy: secretApprovalPolicy, - commiterMembershipId: membership._id.toString(), - data: { - [CommitType.DELETE]: [ - { - secretName - } - ] - } - }); - return res.send({ approval: secretApprovalRequest }); + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (membership && type !== "personal") { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + authData: req.authData, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.DELETE]: [ + { + secretName + } + ] + } + }); + return res.send({ approval: secretApprovalRequest }); + } } } @@ -1155,7 +1176,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req); - const { membership } = await checkSecretsPermission({ + await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -1163,21 +1184,28 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Create }); - if (membership) { - const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); - if (secretApprovalPolicy) { - const secretApprovalRequest = await generateSecretApprovalRequest({ - workspaceId, - environment, - secretPath, - authData: req.authData, - policy: secretApprovalPolicy, - commiterMembershipId: membership._id.toString(), - data: { - [CommitType.CREATE]: secrets.filter(({ type }) => type === "shared") - } - }); - return res.send({ approval: secretApprovalRequest }); + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (membership) { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + authData: req.authData, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.CREATE]: secrets.filter(({ type }) => type === "shared") + } + }); + return res.send({ approval: secretApprovalRequest }); + } } } @@ -1207,7 +1235,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req); - const { membership } = await checkSecretsPermission({ + await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -1215,21 +1243,28 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Edit }); - if (membership) { - const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); - if (secretApprovalPolicy) { - const secretApprovalRequest = await generateSecretApprovalRequest({ - workspaceId, - environment, - secretPath, - policy: secretApprovalPolicy, - commiterMembershipId: membership._id.toString(), - data: { - [CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared") - }, - authData: req.authData - }); - return res.send({ approval: secretApprovalRequest }); + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (membership) { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared") + }, + authData: req.authData + }); + return res.send({ approval: secretApprovalRequest }); + } } } @@ -1258,8 +1293,8 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { const { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req); - - const { membership } = await checkSecretsPermission({ + + await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -1267,21 +1302,28 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Delete }); - if (membership) { - const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); - if (secretApprovalPolicy) { - const secretApprovalRequest = await generateSecretApprovalRequest({ - workspaceId, - environment, - secretPath, - policy: secretApprovalPolicy, - commiterMembershipId: membership._id.toString(), - data: { - [CommitType.DELETE]: secrets.filter(({ type }) => type === "shared") - }, - authData: req.authData - }); - return res.send({ approval: secretApprovalRequest }); + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (membership) { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.DELETE]: secrets.filter(({ type }) => type === "shared") + }, + authData: req.authData + }); + return res.send({ approval: secretApprovalRequest }); + } } } diff --git a/backend/src/controllers/v3/workspacesController.ts b/backend/src/controllers/v3/workspacesController.ts index f1edca1f7..4842ed3be 100644 --- a/backend/src/controllers/v3/workspacesController.ts +++ b/backend/src/controllers/v3/workspacesController.ts @@ -1,9 +1,9 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { validateRequest } from "../../helpers/validation"; -import { Secret, ServiceTokenDataV3 } from "../../models"; +import { Membership, Secret, ServiceTokenDataV3, User } from "../../models"; import { SecretService } from "../../services"; -import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; +import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { UnauthorizedRequestError } from "../../utils/errors"; import * as reqValidator from "../../validation/workspace"; @@ -19,9 +19,22 @@ export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req); - const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); - if (membership.role !== "admin") - throw UnauthorizedRequestError({ message: "User must be an admin" }); + await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (!membership) throw UnauthorizedRequestError(); + + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); + } const secretsWithoutBlindIndex = await Secret.countDocuments({ workspace: new Types.ObjectId(workspaceId), @@ -41,9 +54,22 @@ export const getWorkspaceSecrets = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req); - const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); - if (membership.role !== "admin") - throw UnauthorizedRequestError({ message: "User must be an admin" }); + await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (!membership) throw UnauthorizedRequestError(); + + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); + } const secrets = await Secret.find({ workspace: new Types.ObjectId(workspaceId) @@ -65,9 +91,22 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { body: { secretsToUpdate } } = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req); - const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); - if (membership.role !== "admin") - throw UnauthorizedRequestError({ message: "User must be an admin" }); + await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + + if (req.authData.authPayload instanceof User) { + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (!membership) throw UnauthorizedRequestError(); + + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); + } // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ @@ -109,7 +148,7 @@ export const getWorkspaceServiceTokenData = async (req: Request, res: Response) const serviceTokenData = await ServiceTokenDataV3.find({ workspace: new Types.ObjectId(workspaceId) - }); + }).populate("customRole"); return res.status(200).send({ serviceTokenData diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts index edc31273a..cef14e79f 100644 --- a/backend/src/ee/controllers/v1/roleController.ts +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -1,4 +1,6 @@ import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Membership, User } from "../../../models"; import { CreateRoleSchema, DeleteRoleSchema, @@ -11,7 +13,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, adminProjectPermissions, - getUserProjectPermissions, + getAuthDataProjectPermissions, memberProjectPermissions, viewerProjectPermission } from "../../services/ProjectRoleService"; @@ -39,7 +41,10 @@ export const createRole = async (req: Request, res: Response) => { throw BadRequestError({ message: "user doesn't have the permission." }); } } else { - const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) { throw BadRequestError({ message: "User doesn't have the permission." }); } @@ -82,7 +87,11 @@ export const updateRole = async (req: Request, res: Response) => { throw BadRequestError({ message: "User doesn't have the org permission." }); } } else { - const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) { throw BadRequestError({ message: "User doesn't have the workspace permission." }); } @@ -134,7 +143,11 @@ export const deleteRole = async (req: Request, res: Response) => { throw BadRequestError({ message: "User doesn't have the org permission." }); } } else { - const { permission } = await getUserProjectPermissions(req.user.id, role.workspace.toString()); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: role.workspace + }); + if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) { throw BadRequestError({ message: "User doesn't have the workspace permission." }); } @@ -162,7 +175,11 @@ export const getRoles = async (req: Request, res: Response) => { throw BadRequestError({ message: "User doesn't have the org permission." }); } } else { - const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) { throw BadRequestError({ message: "User doesn't have the workspace permission." }); } @@ -227,7 +244,19 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) = const { params: { workspaceId } } = await validateRequest(GetUserProjectPermission, req); - const { permission, membership } = await getUserProjectPermissions(req.user._id, workspaceId); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + + let membership; + if (req.authData.authPayload instanceof User) { + membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }) + } res.status(200).json({ data: { diff --git a/backend/src/ee/controllers/v1/secretApprovalPolicyController.ts b/backend/src/ee/controllers/v1/secretApprovalPolicyController.ts index eb2f48718..11f1ed557 100644 --- a/backend/src/ee/controllers/v1/secretApprovalPolicyController.ts +++ b/backend/src/ee/controllers/v1/secretApprovalPolicyController.ts @@ -1,10 +1,11 @@ +import { Types } from "mongoose"; import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; import { nanoid } from "nanoid"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import { validateRequest } from "../../../helpers/validation"; import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy"; @@ -19,7 +20,11 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) => body: { approvals, secretPath, approvers, environment, workspaceId, name } } = await validateRequest(reqValidator.CreateSecretApprovalRule, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval @@ -49,10 +54,11 @@ export const updateSecretApprovalPolicy = async (req: Request, res: Response) => const secretApproval = await SecretApprovalPolicy.findById(id); if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND; - const { permission } = await getUserProjectPermissions( - req.user._id, - secretApproval.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: secretApproval.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval @@ -78,10 +84,11 @@ export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => const secretApproval = await SecretApprovalPolicy.findById(id); if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND; - const { permission } = await getUserProjectPermissions( - req.user._id, - secretApproval.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: secretApproval.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval @@ -99,7 +106,11 @@ export const getSecretApprovalPolicy = async (req: Request, res: Response) => { query: { workspaceId } } = await validateRequest(reqValidator.GetSecretApprovalRuleList, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval @@ -117,7 +128,11 @@ export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response query: { workspaceId, environment, secretPath } } = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { secretPath, environment }) diff --git a/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts b/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts index be2e59202..915df5d7c 100644 --- a/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts +++ b/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts @@ -1,7 +1,6 @@ import { Request, Response } from "express"; -import { getUserProjectPermissions } from "../../services/ProjectRoleService"; import { validateRequest } from "../../../helpers/validation"; -import { Folder } from "../../../models"; +import { Folder, Membership, User } from "../../../models"; import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest"; import * as reqValidator from "../../validation/secretApprovalRequest"; import { getFolderWithPathFromId } from "../../../services/FolderService"; @@ -17,7 +16,15 @@ export const getSecretApprovalRequestCount = async (req: Request, res: Response) query: { workspaceId } } = await validateRequest(reqValidator.getSecretApprovalRequestCount, req); - const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (!(req.authData.authPayload instanceof User)) return; + + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (!membership) throw UnauthorizedRequestError(); + const approvalRequestCount = await SecretApprovalRequest.aggregate([ { $match: { @@ -65,7 +72,14 @@ export const getSecretApprovalRequests = async (req: Request, res: Response) => query: { status, committer, workspaceId, environment, limit, offset } } = await validateRequest(reqValidator.getSecretApprovalRequests, req); - const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (!(req.authData.authPayload instanceof User)) return; + + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: new Types.ObjectId(workspaceId) + }); + + if (!membership) throw UnauthorizedRequestError(); const query = { workspace: new Types.ObjectId(workspaceId), @@ -148,10 +162,15 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons if (!secretApprovalRequest) throw BadRequestError({ message: "Secret approval request not found" }); - const { membership } = await getUserProjectPermissions( - req.user._id, - secretApprovalRequest.workspace.toString() - ); + if (!(req.authData.authPayload instanceof User)) return; + + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: secretApprovalRequest.workspace + }); + + if (!membership) throw UnauthorizedRequestError(); + // allow to fetch only if its admin or is the committer or approver if ( membership.role !== "admin" && @@ -190,10 +209,15 @@ export const updateSecretApprovalReviewStatus = async (req: Request, res: Respon if (!secretApprovalRequest) throw BadRequestError({ message: "Secret approval request not found" }); - const { membership } = await getUserProjectPermissions( - req.user._id, - secretApprovalRequest.workspace.toString() - ); + if (!(req.authData.authPayload instanceof User)) return; + + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: secretApprovalRequest.workspace + }); + + if (!membership) throw UnauthorizedRequestError(); + if ( membership.role !== "admin" && secretApprovalRequest.committer !== membership.id && @@ -227,10 +251,15 @@ export const mergeSecretApprovalRequest = async (req: Request, res: Response) => if (!secretApprovalRequest) throw BadRequestError({ message: "Secret approval request not found" }); - const { membership } = await getUserProjectPermissions( - req.user._id, - secretApprovalRequest.workspace.toString() - ); + if (!(req.authData.authPayload instanceof User)) return; + + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: secretApprovalRequest.workspace + }); + + if (!membership) throw UnauthorizedRequestError(); + if ( membership.role !== "admin" && secretApprovalRequest.committer !== membership.id && @@ -272,10 +301,14 @@ export const updateSecretApprovalRequestStatus = async (req: Request, res: Respo if (!secretApprovalRequest) throw BadRequestError({ message: "Secret approval request not found" }); - const { membership } = await getUserProjectPermissions( - req.user._id, - secretApprovalRequest.workspace.toString() - ); + if (!(req.authData.authPayload instanceof User)) return; + + const membership = await Membership.findOne({ + user: req.authData.authPayload._id, + workspace: secretApprovalRequest.workspace + }); + + if (!membership) throw UnauthorizedRequestError(); if ( membership.role !== "admin" && diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 42aa07f3d..ec6018929 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -5,7 +5,7 @@ import { Folder, Secret } from "../../../models"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import { BadRequestError } from "../../../utils/errors"; import * as reqValidator from "../../../validation"; @@ -74,7 +74,11 @@ export const getSecretVersions = async (req: Request, res: Response) => { throw BadRequestError({ message: "Failed to find secret" }); } - const { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString()); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: secret.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback @@ -157,10 +161,12 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { if (!toBeUpdatedSec) { throw BadRequestError({ message: "Failed to find secret" }); } - const { permission } = await getUserProjectPermissions( - req.user._id, - toBeUpdatedSec.workspace.toString() - ); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: toBeUpdatedSec.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback diff --git a/backend/src/ee/controllers/v1/secretRotationController.ts b/backend/src/ee/controllers/v1/secretRotationController.ts index a8a2fd2dd..f2f80ffe6 100644 --- a/backend/src/ee/controllers/v1/secretRotationController.ts +++ b/backend/src/ee/controllers/v1/secretRotationController.ts @@ -1,11 +1,12 @@ import { Request, Response } from "express"; +import { Types } from "mongoose"; import { validateRequest } from "../../../helpers/validation"; import * as reqValidator from "../../validation/secretRotation"; import * as secretRotationService from "../../secretRotation/service"; import { - getUserProjectPermissions, ProjectPermissionActions, - ProjectPermissionSub + ProjectPermissionSub, + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; @@ -23,7 +24,11 @@ export const createSecretRotation = async (req: Request, res: Response) => { } } = await validateRequest(reqValidator.createSecretRotationV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation @@ -49,7 +54,12 @@ export const restartSecretRotations = async (req: Request, res: Response) => { } = await validateRequest(reqValidator.restartSecretRotationV1, req); const doc = await secretRotationService.getSecretRotationById({ id }); - const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString()); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: doc.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation @@ -65,7 +75,12 @@ export const deleteSecretRotations = async (req: Request, res: Response) => { } = await validateRequest(reqValidator.removeSecretRotationV1, req); const doc = await secretRotationService.getSecretRotationById({ id }); - const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString()); + + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: doc.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation @@ -80,7 +95,11 @@ export const getSecretRotations = async (req: Request, res: Response) => { query: { workspaceId } } = await validateRequest(reqValidator.getSecretRotationV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation diff --git a/backend/src/ee/controllers/v1/secretRotationProviderController.ts b/backend/src/ee/controllers/v1/secretRotationProviderController.ts index 7d62ed5f5..5e66c40b2 100644 --- a/backend/src/ee/controllers/v1/secretRotationProviderController.ts +++ b/backend/src/ee/controllers/v1/secretRotationProviderController.ts @@ -1,11 +1,12 @@ import { Request, Response } from "express"; +import { Types } from "mongoose"; import { validateRequest } from "../../../helpers/validation"; import * as reqValidator from "../../validation/secretRotationProvider"; import * as secretRotationProviderService from "../../secretRotation/service"; import { - getUserProjectPermissions, ProjectPermissionActions, - ProjectPermissionSub + ProjectPermissionSub, + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; @@ -14,7 +15,11 @@ export const getProviderTemplates = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(reqValidator.getSecretRotationProvidersV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index e39555929..34a1a6ef2 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -4,7 +4,7 @@ import { validateRequest } from "../../../helpers/validation"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import * as reqValidator from "../../../validation/secretSnapshot"; import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models"; @@ -33,10 +33,11 @@ export const getSecretSnapshot = async (req: Request, res: Response) => { if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - const { permission } = await getUserProjectPermissions( - req.user._id, - secretSnapshot.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: secretSnapshot.workspace + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index b26882d74..7d5caae9b 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -27,7 +27,6 @@ import { } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; -// import Folder, { TFolderSchema } from "../../../models/folder"; import { getFolderByPath, searchByFolderId } from "../../../services/FolderService"; import { EEAuditLogService, EELicenseService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; @@ -46,7 +45,7 @@ import { import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { BadRequestError } from "../../../utils/errors"; @@ -107,7 +106,11 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) = query: { environment, directory, offset, limit } } = await validateRequest(GetWorkspaceSecretSnapshotsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback @@ -148,7 +151,11 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon query: { environment, directory } } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback @@ -238,7 +245,11 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons body: { directory, environment, version } } = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback @@ -572,7 +583,11 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(GetWorkspaceAuditLogsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs @@ -632,7 +647,11 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res params: { workspaceId } } = await validateRequest(GetWorkspaceAuditLogActorFilterOptsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs @@ -700,7 +719,11 @@ export const getWorkspaceTrustedIps = async (req: Request, res: Response) => { params: { workspaceId } } = await validateRequest(GetWorkspaceTrustedIpsV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList @@ -726,7 +749,11 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { body: { comment, isActive, ipAddress: ip } } = await validateRequest(AddWorkspaceTrustedIpV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList @@ -792,7 +819,11 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { body: { ipAddress: ip, comment } } = await validateRequest(UpdateWorkspaceTrustedIpV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList @@ -884,7 +915,11 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { params: { workspaceId, trustedIpId } } = await validateRequest(DeleteWorkspaceTrustedIpV1, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index 4590f3ab8..3e97c9afe 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -8,13 +8,11 @@ import { ServiceTokenDataV3Key, Workspace } from "../../../models"; -import { - IServiceTokenV3Scope, - IServiceTokenV3TrustedIp -} from "../../../models/serviceTokenDataV3"; +import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; import { ActorType, - EventType + EventType, + Role } from "../../models"; import { validateRequest } from "../../../helpers/validation"; import * as reqValidator from "../../../validation/serviceTokenDataV3"; @@ -22,14 +20,14 @@ import { createToken } from "../../../helpers/auth"; import { ProjectPermissionActions, ProjectPermissionSub, - getUserProjectPermissions + getAuthDataProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { EEAuditLogService, EELicenseService } from "../../services"; import { getAuthSecret } from "../../../config"; -import { AuthTokenType } from "../../../variables"; +import { ADMIN, AuthTokenType, CUSTOM, MEMBER, VIEWER } from "../../../variables"; /** * Return project key for service token V3 @@ -163,7 +161,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { name, workspaceId, publicKey, - scopes, + role, trustedIps, expiresIn, accessTokenTTL, @@ -172,7 +170,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => { nonce, // for ServiceTokenDataV3Key } } = await validateRequest(reqValidator.CreateServiceTokenV3, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId) + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens @@ -181,6 +183,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const workspace = await Workspace.findById(workspaceId); if (!workspace) throw BadRequestError({ message: "Workspace not found" }); + const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + + let customRole; + if (isCustomRole) { + customRole = await Role.findOne({ + slug: role, + isOrgRole: false, + workspace: workspace._id + }); + + if (!customRole) throw BadRequestError({ message: "Role not found" }); + } + const plan = await EELicenseService.getPlan(workspace.organization); // validate trusted ips @@ -219,7 +234,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { accessTokenUsageCount: 0, tokenVersion: 1, trustedIps: reformattedTrustedIps, - scopes, + role: isCustomRole ? CUSTOM : role, + customRole, isActive, expiresAt, accessTokenTTL, @@ -250,7 +266,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { metadata: { name, isActive, - scopes: scopes as Array, + role, trustedIps: reformattedTrustedIps as Array, expiresAt } @@ -278,7 +294,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { body: { name, isActive, - scopes, + role, trustedIps, expiresIn, accessTokenTTL, @@ -291,10 +307,10 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { message: "Service token not found" }); - const { permission } = await getUserProjectPermissions( - req.user._id, - serviceTokenData.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: serviceTokenData.workspace + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -304,6 +320,20 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { const workspace = await Workspace.findById(serviceTokenData.workspace); if (!workspace) throw BadRequestError({ message: "Workspace not found" }); + let customRole; + if (role) { + const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + if (isCustomRole) { + customRole = await Role.findOne({ + slug: role, + isOrgRole: false, + workspace: workspace._id + }); + + if (!customRole) throw BadRequestError({ message: "Role not found" }); + } + } + const plan = await EELicenseService.getPlan(workspace.organization); // validate trusted ips @@ -335,7 +365,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { { name, isActive, - scopes, + role: customRole ? CUSTOM : role, + ...(customRole ? { + customRole + } : {}), + ...(role && !customRole ? { // non-custom role + $unset: { + customRole: 1 + } + } : {}), trustedIps: reformattedTrustedIps, expiresAt, accessTokenTTL, @@ -357,7 +395,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { metadata: { name: serviceTokenData.name, isActive, - scopes: scopes as Array, + role, trustedIps: reformattedTrustedIps as Array, expiresAt } @@ -388,10 +426,10 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { message: "Service token not found" }); - const { permission } = await getUserProjectPermissions( - req.user._id, - serviceTokenData.workspace.toString() - ); + const { permission } = await getAuthDataProjectPermissions({ + authData: req.authData, + workspaceId: serviceTokenData.workspace + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -415,7 +453,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { metadata: { name: serviceTokenData.name, isActive: serviceTokenData.isActive, - scopes: serviceTokenData.scopes as Array, + role: serviceTokenData.role, trustedIps: serviceTokenData.trustedIps as Array, expiresAt: serviceTokenData.expiresAt } diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 6f9971ef7..48147db08 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -2,7 +2,7 @@ export enum ActorType { USER = "user", SERVICE = "service", SERVICE_V3 = "service-v3", - Machine = "machine" + // Machine = "machine" } export enum UserAgentType { diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 51f94fdb0..bae4e2f96 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -1,5 +1,5 @@ import { ActorType, EventType } from "./enums"; -import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; +import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; interface UserActorMetadata { userId: string; @@ -26,11 +26,11 @@ export interface ServiceActorV3 { metadata: ServiceActorMetadata; } -export interface MachineActor { - type: ActorType.Machine; -} +// export interface MachineActor { +// type: ActorType.Machine; +// } -export type Actor = UserActor | ServiceActor | ServiceActorV3 | MachineActor; +export type Actor = UserActor | ServiceActor | ServiceActorV3; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -225,7 +225,7 @@ interface CreateServiceTokenV3Event { metadata: { name: string; isActive: boolean; - scopes: Array; + role: string; trustedIps: Array; expiresAt?: Date; }; @@ -236,7 +236,7 @@ interface UpdateServiceTokenV3Event { metadata: { name?: string; isActive?: boolean; - scopes?: Array; + role?: string; trustedIps?: Array; expiresAt?: Date; }; @@ -247,7 +247,7 @@ interface DeleteServiceTokenV3Event { metadata: { name: string; isActive: boolean; - scopes: Array; + role: string; expiresAt?: Date; trustedIps: Array; }; diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index ac71121de..4e882d00f 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -1,3 +1,4 @@ +import { Types } from "mongoose"; import { AbilityBuilder, ForcedSubject, @@ -6,11 +7,14 @@ import { buildMongoQueryMatcher, createMongoAbility } from "@casl/ability"; -import { Membership } from "../../models"; -import { IRole } from "../models/role"; -import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { UnauthorizedRequestError } from "../../utils/errors"; import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; import picomatch from "picomatch"; +import { AuthData } from "../../interfaces/middleware"; +import { ActorType, IRole } from "../models"; +import { Membership, ServiceTokenData, ServiceTokenDataV3 } from "../../models"; +import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; +import { checkIPAgainstBlocklist } from "../../utils/ip"; const $glob: FieldInstruction = { type: "field", @@ -239,31 +243,89 @@ const buildViewerPermission = () => { 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(); +/** + * Return permissions for user/service pertaining to workspace with id [workspaceId] + * + * Note: should not rely on this function for ST V2 authorization logic + * b/c ST V2 does not support role-based access control + */ +export const getAuthDataProjectPermissions = async ({ + authData, + workspaceId +}: { + authData: AuthData; + workspaceId: Types.ObjectId; +}) => { + let role: "admin" | "member" | "viewer" | "custom"; + let customRole; + + switch (authData.actor.type) { + case ActorType.USER: { + const membership = await Membership.findOne({ + user: authData.authPayload._id, + workspace: workspaceId + }) + .populate<{ + customRole: IRole & { permissions: RawRuleOf>[] }; + }>("customRole") + .exec(); + + if (!membership || (membership.role === "custom" && !membership.customRole)) { + throw UnauthorizedRequestError(); + } + + role = membership.role; + customRole = membership.customRole; + break; + } + case ActorType.SERVICE: { + const serviceTokenData = await ServiceTokenData.findById(authData.authPayload._id); + if (!serviceTokenData || !serviceTokenData.workspace.equals(workspaceId)) throw UnauthorizedRequestError(); + role = "viewer"; + break; + } + case ActorType.SERVICE_V3: { + const serviceTokenData = await ServiceTokenDataV3 + .findById(authData.authPayload._id) + .populate<{ + customRole: IRole & { permissions: RawRuleOf>[] }; + }>("customRole") + .exec(); + + if (!serviceTokenData || (serviceTokenData.role === "custom" && !serviceTokenData.customRole)) { + throw UnauthorizedRequestError(); + } - if (!membership || (membership.role === "custom" && !membership.customRole)) { - throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); + checkIPAgainstBlocklist({ + ipAddress: authData.ipAddress, + trustedIps: serviceTokenData.trustedIps + }); + + role = serviceTokenData.role; + customRole = serviceTokenData.customRole; + break; + } + default: + throw UnauthorizedRequestError(); } - 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 }; + switch (role) { + case ADMIN: + return { permission: adminProjectPermissions }; + case MEMBER: + return { permission: memberProjectPermissions }; + case VIEWER: + return { permission: viewerProjectPermission }; + case CUSTOM: { + if (!customRole) throw UnauthorizedRequestError(); + return { + permission: createMongoAbility( + customRole.permissions, + { conditionsMatcher } + ) + }; + } + default: + throw UnauthorizedRequestError(); } - - throw BadRequestError({ message: "User role not found" }); -}; +} diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 6bd2db1cd..a3eb541bf 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -13,13 +13,11 @@ import { Folder, ISecret, IServiceTokenData, - IServiceTokenDataV3, Secret, SecretBlindIndexData, ServiceTokenData, TFolderRootSchema } from "../models"; -import { Permission } from "../models/serviceTokenDataV3"; import { EventType, SecretVersion } from "../ee/models"; import { BadRequestError, @@ -51,42 +49,6 @@ import picomatch from "picomatch"; import path from "path"; import { getAnImportedSecret } from "../services/SecretImportService"; -/** - * Validate scope for service token v3 - * @param authPayload - * @param environment - * @param secretPath - * @returns - */ -export const isValidScopeV3 = ({ - authPayload, - environment, - secretPath, - requiredPermissions -}: { - authPayload: IServiceTokenDataV3; - environment: string; - secretPath: string; - requiredPermissions: Permission[]; -}) => { - const { scopes } = authPayload; - - const validScope = scopes.find( - (scope) => - picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && - scope.environment === environment - ); - - if ( - validScope && - !requiredPermissions.every((permission) => validScope.permissions.includes(permission)) - ) { - return false; - } - - return Boolean(validScope); -}; - /** * Validate scope for service token v2 * @param authPayload diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index 86b89d63f..fb39613d6 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -1,16 +1,6 @@ import { Document, Schema, Types, model } from "mongoose"; import { IPType } from "../ee/models"; - -export enum Permission { - READ = "read", - WRITE = "write" -} - -export interface IServiceTokenV3Scope { - environment: string; - secretPath: string; - permissions: Permission[]; -} +import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; export interface IServiceTokenV3TrustedIp { ipAddress: string; @@ -33,7 +23,8 @@ export interface IServiceTokenDataV3 extends Document { isRefreshTokenRotationEnabled: boolean; expiresAt?: Date; accessTokenTTL: number; - scopes: Array; + role: "admin" | "member" | "viewer" | "custom"; + customRole: Types.ObjectId; trustedIps: Array; } @@ -100,28 +91,15 @@ const serviceTokenDataV3Schema = new Schema( default: 7200, required: true }, - scopes: { // TODO: consider switching this out for roles instead - type: [ - { - environment: { - type: String, - required: true - }, - secretPath: { - type: String, - default: "/", - required: true - }, - permissions: { - type: [String], - enum: [Permission.READ, Permission.WRITE], - default: [Permission.READ], - required: true - } - } - ], + role: { + type: String, + enum: [ADMIN, MEMBER, VIEWER, CUSTOM], required: true }, + customRole: { + type: Schema.Types.ObjectId, + ref: "Role" + }, trustedIps: { type: [ { diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index 9682a04a2..49e1786b3 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -36,12 +36,4 @@ router.delete( environmentController.deleteWorkspaceEnvironment ); -router.get( - "/:workspaceId/environments", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] - }), - environmentController.getAllAccessibleEnvironmentsOfWorkspace -); - export default router; diff --git a/backend/src/validation/serviceTokenDataV3.ts b/backend/src/validation/serviceTokenDataV3.ts index 4be60c645..e4b2603b3 100644 --- a/backend/src/validation/serviceTokenDataV3.ts +++ b/backend/src/validation/serviceTokenDataV3.ts @@ -1,64 +1,5 @@ -import { Types } from "mongoose"; -import { IServiceTokenDataV3 } from "../models"; -import { Permission } from "../models/serviceTokenDataV3"; import { z } from "zod"; -import { UnauthorizedRequestError } from "../utils/errors"; -import { isValidScopeV3 } from "../helpers"; -import { AuthData } from "../interfaces/middleware"; -import { checkIPAgainstBlocklist } from "../utils/ip"; - -/** - * Validate that service token (client) can access workspace - * with id [workspaceId] and its environment [environment] with required permissions - * [requiredPermissions] - * @param {Object} obj - * @param {ServiceTokenData} obj.serviceTokenData - service token client - * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against - * @param {String} environment - (optional) environment in workspace to validate against - * @param {String[]} acceptedPermissions - accepted permissions as part of the endpoint - */ - export const validateServiceTokenDataV3ClientForWorkspace = async ({ - authData, - serviceTokenData, - workspaceId, - environment, - secretPath = "/", - requiredPermissions -}: { - authData: AuthData; - serviceTokenData: IServiceTokenDataV3; - workspaceId: Types.ObjectId; - environment?: string; - secretPath?: string; - requiredPermissions: Permission[]; -}) => { - - // validate ST V3 IP address - checkIPAgainstBlocklist({ - ipAddress: authData.ipAddress, - trustedIps: serviceTokenData.trustedIps - }); - - if (!serviceTokenData.workspace.equals(workspaceId)) { - // case: invalid workspaceId passed - throw UnauthorizedRequestError({ - message: "Failed service token authorization for the given workspace" - }); - } - - if (environment) { - const isValid = isValidScopeV3({ - authPayload: serviceTokenData, - environment, - secretPath, - requiredPermissions - }); - - if (!isValid) throw UnauthorizedRequestError({ - message: "Failed service token authorization for the given workspace" - }); - } -}; +import { MEMBER } from "../variables"; export const RefreshTokenV3 = z.object({ body: z.object({ @@ -71,20 +12,14 @@ export const CreateServiceTokenV3 = z.object({ name: z.string().trim(), workspaceId: z.string().trim(), publicKey: z.string().trim(), - scopes: z - .object({ - permissions: z.enum(["read", "write"]).array(), - environment: z.string().trim(), - secretPath: z.string().trim() - }) - .array() - .min(1), - trustedIps: z + role: z.string().trim().min(1).default(MEMBER), + trustedIps: z // TODO: provide default .object({ ipAddress: z.string().trim(), }) .array() - .min(1), + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }]), expiresIn: z.number().optional(), accessTokenTTL: z.number().int().min(1), encryptedKey: z.string().trim(), @@ -100,15 +35,7 @@ export const UpdateServiceTokenV3 = z.object({ body: z.object({ name: z.string().trim().optional(), isActive: z.boolean().optional(), - scopes: z - .object({ - permissions: z.enum(["read", "write"]).array(), - environment: z.string().trim(), - secretPath: z.string().trim() - }) - .array() - .min(1) - .optional(), + role: z.string().trim().min(1).optional(), trustedIps: z .object({ ipAddress: z.string().trim() diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index f28f9004c..8680c9bed 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -4,17 +4,6 @@ import { UserAgentType } from "./enums"; -enum Permission { - READ = "read", - READ_WRITE = "readWrite" -} - -interface Scope { - environment: string; - secretPath: string; - permission: Permission; -} - interface UserActorMetadata { userId: string; email: string; @@ -211,7 +200,7 @@ interface CreateServiceTokenV3Event { metadata: { name: string; isActive: boolean; - scopes: Array; + role: string; expiresAt?: Date; } } @@ -221,7 +210,7 @@ interface UpdateServiceTokenV3Event { metadata: { name?: string; isActive?: boolean; - scopes?: Array; + role?: string; expiresAt?: Date; } } @@ -231,7 +220,7 @@ interface DeleteServiceTokenV3Event { metadata: { name: string; isActive: boolean; - scopes: Array; + role?: string; expiresAt?: Date; } } diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 44786ba0d..d0bf2ac42 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -85,8 +85,8 @@ export const useUpdateServiceTokenV3 = () => { mutationFn: async ({ serviceTokenDataId, name, + role, isActive, - scopes, trustedIps, expiresIn, accessTokenTTL, @@ -94,8 +94,8 @@ export const useUpdateServiceTokenV3 = () => { }) => { const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { name, + role, isActive, - scopes, trustedIps, expiresIn, accessTokenTTL, diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index 18e59c68e..210ff7cd8 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -1,5 +1,3 @@ -import { Permission } from "./enums"; - export type ServiceTokenScope = { environment: string; secretPath: string; @@ -38,12 +36,6 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken }; // --- v3 -export type ServiceTokenV3Scope = { - permissions: Permission[]; - environment: string; - secretPath: string; -}; - export type ServiceTokenV3TrustedIp = { _id: string; ipAddress: string; @@ -54,13 +46,17 @@ export type ServiceTokenV3TrustedIp = { export type ServiceTokenDataV3 = { _id: string; name: string; + role: string; + customRole?: { + name: string; + slug: string; + }; workspace: string; isActive: boolean; refreshTokenLastUsed?: string; accessTokenLastUsed?: string; refreshTokenUsageCount: number; accessTokenUsageCount: number; - scopes: ServiceTokenV3Scope[]; trustedIps: ServiceTokenV3TrustedIp[]; expiresAt?: string; accessTokenTTL: number; @@ -71,9 +67,9 @@ export type ServiceTokenDataV3 = { export type CreateServiceTokenDataV3DTO = { name: string; + role?: string; workspaceId: string; publicKey: string; - scopes: ServiceTokenV3Scope[]; trustedIps: { ipAddress: string; }[]; @@ -93,7 +89,7 @@ export type UpdateServiceTokenDataV3DTO = { serviceTokenDataId: string; isActive?: boolean; name?: string; - scopes?: ServiceTokenV3Scope[]; + role?: string; trustedIps?: { ipAddress: string; }[]; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index f7b749156..cef88fede 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -7,7 +7,6 @@ export { useDeleteWsEnvironment, useGetUserWorkspaceMemberships, useGetUserWorkspaces, - useGetUserWsEnvironments, useGetWorkspaceAuthorizations, useGetWorkspaceById, useGetWorkspaceIndexStatus, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 5b185b97a..ede931de8 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -12,14 +12,12 @@ import { CreateWorkspaceDTO, DeleteEnvironmentDTO, DeleteWorkspaceDTO, - GetWsEnvironmentDTO, NameWorkspaceSecretsDTO, RenameWorkspaceDTO, ReorderEnvironmentsDTO, ToggleAutoCapitalizationDTO, UpdateEnvironmentDTO, - Workspace, - WorkspaceEnv + Workspace } from "./types"; export const workspaceKeys = { @@ -31,7 +29,6 @@ export const workspaceKeys = { getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], getAllUserWorkspace: ["workspaces"] as const, - getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const, getWorkspaceServiceTokenDataV3: (workspaceId: string) => @@ -103,21 +100,6 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => { return data; }; -const fetchUserWsEnvironments = async (workspaceId: string) => { - const { data } = await apiRequest.get<{ accessibleEnvironments: WorkspaceEnv[] }>( - `/api/v2/workspace/${workspaceId}/environments` - ); - return data.accessibleEnvironments; -}; - -export const useGetUserWsEnvironments = ({ workspaceId, onSuccess }: GetWsEnvironmentDTO) => - useQuery({ - enabled: Boolean(workspaceId), - onSuccess, - queryKey: workspaceKeys.getUserWsEnvironments(workspaceId), - queryFn: () => fetchUserWsEnvironments(workspaceId) - }); - // to get all userids in an org with the workspace they are part of export const useGetUserWorkspaceMemberships = (orgId: string) => useQuery({ diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index f6728988a..bebf5119f 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -30,11 +30,6 @@ export type CreateWorkspaceDTO = { organizationId: string; }; -export type GetWsEnvironmentDTO = { - workspaceId: string; - onSuccess?: (data: WorkspaceEnv[]) => void; -}; - export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean }; diff --git a/frontend/src/views/Project/MembersPage/MembersPage.tsx b/frontend/src/views/Project/MembersPage/MembersPage.tsx index e14562c08..a9b10b607 100644 --- a/frontend/src/views/Project/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Project/MembersPage/MembersPage.tsx @@ -2,7 +2,7 @@ import { motion } from "framer-motion"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; import { MemberListTab } from "./components/MemberListTab"; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx index c0499d243..758970e2d 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx @@ -74,6 +74,7 @@ export const MemberListTab = () => { orgId, workspaceId }); + const { data: wsKey } = useGetUserWsKey(workspaceId); const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); const { data: orgUsers } = useGetOrgUsers(orgId); diff --git a/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/ServiceTokenTab.tsx b/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/ServiceTokenTab.tsx index 86ae8bfe5..1a3c6e95c 100644 --- a/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/ServiceTokenTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/ServiceTokenTab.tsx @@ -2,7 +2,7 @@ import { motion } from "framer-motion"; import { ServiceTokenSection, - // ServiceTokenV3Section + ServiceTokenV3Section } from "./components"; export const ServiceTokenTab = () => { @@ -14,7 +14,7 @@ export const ServiceTokenTab = () => { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > - {/* */} + ); diff --git a/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index 3d76b2435..cfe5937d7 100644 --- a/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -29,22 +29,17 @@ import { Tabs, UpgradePlanModal} from "@app/components/v2"; import { + useOrganization, useSubscription, - useWorkspace + useWorkspace } from "@app/context"; import { useToggle } from "@app/hooks"; import { useCreateServiceTokenV3, + useGetRoles, useGetUserWsKey, - useUpdateServiceTokenV3 -} from "@app/hooks/api"; -import { - Permission -} from "@app/hooks/api/serviceTokens/enums"; -import { - ServiceTokenV3Scope, - ServiceTokenV3TrustedIp -} from "@app/hooks/api/serviceTokens/types"; + useUpdateServiceTokenV3} from "@app/hooks/api"; +import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; enum TabSections { @@ -61,13 +56,6 @@ const expirations = [ { label: "12 months", value: "31104000" } ]; -const permissionsMap: { - [key: string]: Permission[] -} = { - "read": [Permission.READ], - "readWrite": [Permission.READ, Permission.WRITE], -} - const schema = yup.object({ name: yup.string().required("ST V3 name is required"), expiresIn: yup.string(), @@ -82,24 +70,7 @@ const schema = yup.object({ return !Number.isNaN(num) && num > 0 && String(num) === value; }) .required("Access Token TTL is required"), - scopes: yup - .array( - yup.object({ - permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"), - environment: yup.string().max(50).required().label("Environment"), - secretPath: yup - .string() - .required() - .default("/") - .label("Secret Path") - .transform((val) => - typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val - ) - }) - ) - .min(1) - .required() - .label("Scope"), + role: yup.string().required("ST V3 role is required"), trustedIps: yup .array( yup.object({ @@ -129,9 +100,18 @@ export const AddServiceTokenV3Modal = ({ const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false); const { subscription } = useSubscription(); + const { currentOrg } = useOrganization(); const { currentWorkspace } = useWorkspace(); - const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); + const orgId = currentOrg?._id || ""; + const workspaceId = currentWorkspace?._id || ""; + + const { data: roles } = useGetRoles({ + orgId, + workspaceId + }); + + const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3(); const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); const { createNotification } = useNotificationContext(); @@ -145,11 +125,6 @@ export const AddServiceTokenV3Modal = ({ defaultValues: { name: "", accessTokenTTL: "7200", - scopes: [{ - permission: "read", - environment: currentWorkspace?.environments?.[0]?.slug, - secretPath: "/", - }], trustedIps: [{ ipAddress: "0.0.0.0/0" }] @@ -175,31 +150,22 @@ export const AddServiceTokenV3Modal = ({ const serviceTokenData = popUp?.serviceTokenV3?.data as { serviceTokenDataId: string; name: string; - scopes: ServiceTokenV3Scope[]; + role: string; + customRole: { + name: string; + slug: string; + }; trustedIps: ServiceTokenV3TrustedIp[]; accessTokenTTL: number; isRefreshTokenRotationEnabled: boolean; }; + if (!roles?.length) return; + if (serviceTokenData) { reset({ name: serviceTokenData.name, - scopes: serviceTokenData.scopes.map(({ - environment, - secretPath, - permissions - }: ServiceTokenV3Scope) => { - let permission = "read"; - if (permissions.includes(Permission.WRITE)) { - permission = "readWrite"; - } - - return ({ - environment, - secretPath, - permission - }) - }), + role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role, trustedIps: serviceTokenData.trustedIps.map(({ ipAddress, prefix @@ -215,26 +181,21 @@ export const AddServiceTokenV3Modal = ({ reset({ name: "", accessTokenTTL: "7200", - scopes: [{ - permission: "read", - environment: currentWorkspace?.environments?.[0]?.slug, - secretPath: "/", - }], + role: roles[0].slug, trustedIps: [{ ipAddress: "0.0.0.0/0" }] }); } - }, [popUp?.serviceTokenV3?.data]); + }, [popUp?.serviceTokenV3?.data, roles]); - const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" }); const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" }); const onFormSubmit = async ({ name, expiresIn, accessTokenTTL, - scopes, + role, trustedIps, isRefreshTokenRotationEnabled }: FormData) => { @@ -242,25 +203,16 @@ export const AddServiceTokenV3Modal = ({ const serviceTokenData = popUp?.serviceTokenV3?.data as { serviceTokenDataId: string; name: string; - scopes: any; + role: string; }; - // convert read/readWrite permission => ["read", "write"] format - const reformattedScopes = scopes.map((scope) => { - return ({ - environment: scope.environment, - secretPath: scope.secretPath, - permissions: permissionsMap[scope.permission] - }); - }); - if (serviceTokenData) { // update await updateMutateAsync({ serviceTokenDataId: serviceTokenData.serviceTokenDataId, name, - scopes: reformattedScopes, + role, trustedIps, expiresIn: expiresIn === "" ? undefined : Number(expiresIn), accessTokenTTL: Number(accessTokenTTL), @@ -270,7 +222,7 @@ export const AddServiceTokenV3Modal = ({ handlePopUpToggle("serviceTokenV3", false); } else { // create - if (!currentWorkspace?._id) return; + if (!workspaceId) return; if (!latestFileKey) return; const pair = nacl.box.keyPair(); @@ -294,9 +246,9 @@ export const AddServiceTokenV3Modal = ({ const { refreshToken } = await createMutateAsync({ name, - workspaceId: currentWorkspace._id, + role, + workspaceId, publicKey, - scopes: reformattedScopes, trustedIps, expiresIn: expiresIn === "" ? undefined : Number(expiresIn), accessTokenTTL: Number(accessTokenTTL), @@ -385,102 +337,32 @@ export const AddServiceTokenV3Modal = ({ )} /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - onChange(e)} + className="w-full" + > + {(roles || []).map(({ name, slug }) => ( + {name} - ))} - - - )} - /> - ( - - - - )} - /> - remove(index)} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
+ ))} + + + )} + /> Name Status - Scopes + Role Trusted IPs Access Token TTL Created At @@ -98,7 +101,8 @@ export const ServiceTokenV3Table = ({ _id, name, isActive, - scopes, + role, + customRole, trustedIps, createdAt, expiresAt, @@ -128,26 +132,7 @@ export const ServiceTokenV3Table = ({ )} - - {scopes.map((scope) => { - let permissionText = "read" - if ( - scope.permissions.includes(Permission.WRITE) && - scope.permissions.includes(Permission.READ) - ) { - permissionText = "readWrite"; - } - - return ( -

- - {permissionText} - - {` @${scope.environment} - ${scope.secretPath}`} -

- ); - })} - + {customRole?.slug ?? role} {trustedIps.map(({ _id: trustedIpId, @@ -175,7 +160,8 @@ export const ServiceTokenV3Table = ({ handlePopUpOpen("serviceTokenV3", { serviceTokenDataId: _id, name, - scopes, + role, + customRole, trustedIps, accessTokenTTL, isRefreshTokenRotationEnabled diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx deleted file mode 100644 index 7910652b5..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import crypto from "crypto"; - -import { useEffect, useState } from "react"; -import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { faCheck, faCopy, faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - decryptAssymmetric, - encryptSymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { - Button, - Checkbox, - FormControl, - IconButton, - Input, - Modal, - ModalClose, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { useToggle } from "@app/hooks"; -import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -const apiTokenExpiry = [ - { label: "1 Day", value: 86400 }, - { label: "7 Days", value: 604800 }, - { label: "1 Month", value: 2592000 }, - { label: "6 months", value: 15552000 }, - { label: "12 months", value: 31104000 }, - { label: "Never", value: null } -]; - -const schema = yup.object({ - name: yup.string().max(100).required().label("Service Token Name"), - scopes: yup - .array( - yup.object({ - environment: yup.string().max(50).required().label("Environment"), - secretPath: yup - .string() - .required() - .default("/") - .label("Secret Path") - .transform((val) => - typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val - ) - }) - ) - .min(1) - .required() - .label("Scope"), - expiresIn: yup.string().optional().label("Service Token Expiration"), - permissions: yup - .object() - .shape({ - read: yup.boolean().required(), - write: yup.boolean().required() - }) - .defined() - .required() -}); - -export type FormData = yup.InferType; - -type Props = { - popUp: UsePopUpState<["createAPIToken"]>; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void; -}; - -export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { - const { t } = useTranslation(); - const { createNotification } = useNotificationContext(); - const { currentWorkspace } = useWorkspace(); - const { - control, - reset, - handleSubmit, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - scopes: [{ - secretPath: "/", - environment: currentWorkspace?.environments?.[0]?.slug - }] - } - }); - - const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" }); - - const [newToken, setToken] = useState(""); - const [isTokenCopied, setIsTokenCopied] = useToggle(false); - - const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); - const createServiceToken = useCreateServiceToken(); - const hasServiceToken = Boolean(newToken); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isTokenCopied) { - timer = setTimeout(() => setIsTokenCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [isTokenCopied]); - - const copyTokenToClipboard = () => { - navigator.clipboard.writeText(newToken); - setIsTokenCopied.on(); - }; - - const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => { - try { - if (!currentWorkspace?._id) return; - if (!latestFileKey) return; - - const key = decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY") as string - }); - - const randomBytes = crypto.randomBytes(16).toString("hex"); - - const { ciphertext, iv, tag } = encryptSymmetric({ - plaintext: key, - key: randomBytes - }); - - const { serviceToken } = await createServiceToken.mutateAsync({ - encryptedKey: ciphertext, - iv, - tag, - scopes, - expiresIn: Number(expiresIn), - name, - workspaceId: currentWorkspace._id, - randomBytes, - permissions: Object.entries(permissions) - .filter(([, permissionsValue]) => permissionsValue) - .map(([permissionsKey]) => permissionsKey) - }); - - setToken(serviceToken); - createNotification({ - text: "Successfully created a service token", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create a service token", - type: "error" - }); - } - }; - - return ( - { - handlePopUpToggle("createAPIToken", open); - reset(); - setToken(""); - }} - > - - {!hasServiceToken ? ( -
- ( - - - - )} - /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - > - - -
- ))} -
- -
- ( - - - - )} - /> - { - const options = [ - { - label: "Read (default)", - value: "read" - }, - { - label: "Write (optional)", - value: "write" - } - ]; - - return ( - - <> - {options.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} - - - ); - }} - /> -
- - - - -
- - ) : ( -
-

{newToken}

- - - - {t("common.click-to-copy")} - - -
- )} -
-
- ); -}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx deleted file mode 100644 index 94a185871..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; -import { withProjectPermission } from "@app/hoc"; -import { usePopUp } from "@app/hooks"; -import { useDeleteServiceToken } from "@app/hooks/api"; - -import { AddServiceTokenModal } from "./AddServiceTokenModal"; -import { ServiceTokenTable } from "./ServiceTokenTable"; - -type DeleteModalData = { name: string; id: string }; - -export const ServiceTokenSection = withProjectPermission( - () => { - const { t } = useTranslation(); - const { createNotification } = useNotificationContext(); - const deleteServiceToken = useDeleteServiceToken(); - - const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ - "createAPIToken", - "deleteAPITokenConfirmation" - ] as const); - - const onDeleteApproved = async () => { - try { - deleteServiceToken.mutateAsync( - (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id - ); - createNotification({ - text: "Successfully deleted service token", - type: "success" - }); - - handlePopUpClose("deleteAPITokenConfirmation"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete service token", - type: "error" - }); - } - }; - - return ( -
-
-

- Service Tokens -

- - {(isAllowed) => ( - - )} - -
-

{t("section.token.service-tokens-description")}

- - - handlePopUpToggle("deleteAPITokenConfirmation", isOpen)} - deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name} - onClose={() => handlePopUpClose("deleteAPITokenConfirmation")} - onDeleteApproved={onDeleteApproved} - /> -
- ); - }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens } -); diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx deleted file mode 100644 index 6ac54ba97..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - EmptyState, - IconButton, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { useGetUserWsServiceTokens } from "@app/hooks/api"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>, - { - name, - id - }: { - name: string; - id: string; - } - ) => void; -}; - -export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useGetUserWsServiceTokens({ - workspaceID: currentWorkspace?._id || "" - }); - - return ( - - - - - - - - - - - {isLoading && } - {!isLoading && - data && - data.map((row) => ( - - - - - - - ))} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
Token NameEnvironment - Secret PathValid Until -
{row.name} -
- {row?.scopes.map(({ secretPath, environment }) => ( -
-
{environment}
- - {secretPath} -
- ))} -
-
{row.expiresAt && new Date(row.expiresAt).toUTCString()} - - {(isAllowed) => ( - - handlePopUpOpen("deleteAPITokenConfirmation", { - name: row.name, - id: row._id - }) - } - colorSchema="danger" - ariaLabel="delete" - isDisabled={!isAllowed} - > - - - )} - -
- -
-
- ); -}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/index.tsx deleted file mode 100644 index 588dd234d..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {ServiceTokenSection} from "./ServiceTokenSection" \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx deleted file mode 100644 index 3d76b2435..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ /dev/null @@ -1,657 +0,0 @@ -import { useEffect, useState } from "react"; -import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import { motion } from "framer-motion"; -import nacl from "tweetnacl"; -import { encodeBase64 } from "tweetnacl-util"; -import * as yup from "yup"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - decryptAssymmetric, - encryptAssymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { - Button, - FormControl, - IconButton, - Input, - Modal, - ModalContent, - Select, - SelectItem, - Switch, - Tab, - TabList, - TabPanel, - Tabs, - UpgradePlanModal} from "@app/components/v2"; -import { - useSubscription, - useWorkspace -} from "@app/context"; -import { useToggle } from "@app/hooks"; -import { - useCreateServiceTokenV3, - useGetUserWsKey, - useUpdateServiceTokenV3 -} from "@app/hooks/api"; -import { - Permission -} from "@app/hooks/api/serviceTokens/enums"; -import { - ServiceTokenV3Scope, - ServiceTokenV3TrustedIp -} from "@app/hooks/api/serviceTokens/types"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -enum TabSections { - General = "general", - Advanced = "advanced" -} - -const expirations = [ - { label: "Never", value: "" }, - { label: "1 day", value: "86400" }, - { label: "7 days", value: "604800" }, - { label: "1 month", value: "2592000" }, - { label: "6 months", value: "15552000" }, - { label: "12 months", value: "31104000" } -]; - -const permissionsMap: { - [key: string]: Permission[] -} = { - "read": [Permission.READ], - "readWrite": [Permission.READ, Permission.WRITE], -} - -const schema = yup.object({ - name: yup.string().required("ST V3 name is required"), - expiresIn: yup.string(), - accessTokenTTL: yup - .string() - .test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => { - if (typeof value === "undefined") { - return false; - } - - const num = parseInt(value, 10); - return !Number.isNaN(num) && num > 0 && String(num) === value; - }) - .required("Access Token TTL is required"), - scopes: yup - .array( - yup.object({ - permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"), - environment: yup.string().max(50).required().label("Environment"), - secretPath: yup - .string() - .required() - .default("/") - .label("Secret Path") - .transform((val) => - typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val - ) - }) - ) - .min(1) - .required() - .label("Scope"), - trustedIps: yup - .array( - yup.object({ - ipAddress: yup.string().max(50).required().label("IP Address") - }) - ) - .min(1) - .required() - .label("Trusted IP"), - isRefreshTokenRotationEnabled: yup.boolean().default(false) -}).required(); - -export type FormData = yup.InferType; - -type Props = { - popUp: UsePopUpState<["serviceTokenV3", "upgradePlan"]>; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["serviceTokenV3", "upgradePlan"]>, state?: boolean) => void; -}; - -export const AddServiceTokenV3Modal = ({ - popUp, - handlePopUpOpen, - handlePopUpToggle -}: Props) => { - const [newServiceTokenJSON, setNewServiceTokenJSON] = useState(""); - const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false); - - const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); - - const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); - const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3(); - const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); - const { createNotification } = useNotificationContext(); - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - name: "", - accessTokenTTL: "7200", - scopes: [{ - permission: "read", - environment: currentWorkspace?.environments?.[0]?.slug, - secretPath: "/", - }], - trustedIps: [{ - ipAddress: "0.0.0.0/0" - }] - } - }); - - useEffect(() => { - let timer: NodeJS.Timeout; - - if (isServiceTokenJSONCopied) { - timer = setTimeout(() => setIsServiceTokenJSONCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [setIsServiceTokenJSONCopied]); - - const copyTokenToClipboard = () => { - navigator.clipboard.writeText(newServiceTokenJSON); - setIsServiceTokenJSONCopied.on(); - }; - - useEffect(() => { - const serviceTokenData = popUp?.serviceTokenV3?.data as { - serviceTokenDataId: string; - name: string; - scopes: ServiceTokenV3Scope[]; - trustedIps: ServiceTokenV3TrustedIp[]; - accessTokenTTL: number; - isRefreshTokenRotationEnabled: boolean; - }; - - if (serviceTokenData) { - reset({ - name: serviceTokenData.name, - scopes: serviceTokenData.scopes.map(({ - environment, - secretPath, - permissions - }: ServiceTokenV3Scope) => { - let permission = "read"; - if (permissions.includes(Permission.WRITE)) { - permission = "readWrite"; - } - - return ({ - environment, - secretPath, - permission - }) - }), - trustedIps: serviceTokenData.trustedIps.map(({ - ipAddress, - prefix - }: ServiceTokenV3TrustedIp) => { - return ({ - ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` - }); - }), - accessTokenTTL: String(serviceTokenData.accessTokenTTL), - isRefreshTokenRotationEnabled: serviceTokenData.isRefreshTokenRotationEnabled - }); - } else { - reset({ - name: "", - accessTokenTTL: "7200", - scopes: [{ - permission: "read", - environment: currentWorkspace?.environments?.[0]?.slug, - secretPath: "/", - }], - trustedIps: [{ - ipAddress: "0.0.0.0/0" - }] - }); - } - }, [popUp?.serviceTokenV3?.data]); - - const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" }); - const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" }); - - const onFormSubmit = async ({ - name, - expiresIn, - accessTokenTTL, - scopes, - trustedIps, - isRefreshTokenRotationEnabled - }: FormData) => { - try { - const serviceTokenData = popUp?.serviceTokenV3?.data as { - serviceTokenDataId: string; - name: string; - scopes: any; - }; - - // convert read/readWrite permission => ["read", "write"] format - const reformattedScopes = scopes.map((scope) => { - return ({ - environment: scope.environment, - secretPath: scope.secretPath, - permissions: permissionsMap[scope.permission] - }); - }); - - if (serviceTokenData) { - // update - - await updateMutateAsync({ - serviceTokenDataId: serviceTokenData.serviceTokenDataId, - name, - scopes: reformattedScopes, - trustedIps, - expiresIn: expiresIn === "" ? undefined : Number(expiresIn), - accessTokenTTL: Number(accessTokenTTL), - isRefreshTokenRotationEnabled - }); - - handlePopUpToggle("serviceTokenV3", false); - } else { - // create - if (!currentWorkspace?._id) return; - if (!latestFileKey) return; - - const pair = nacl.box.keyPair(); - const secretKeyUint8Array = pair.secretKey; - const publicKeyUint8Array = pair.publicKey; - const privateKey = encodeBase64(secretKeyUint8Array); - const publicKey = encodeBase64(publicKeyUint8Array); - - const key = decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY") as string - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY") as string - }); - - const { refreshToken } = await createMutateAsync({ - name, - workspaceId: currentWorkspace._id, - publicKey, - scopes: reformattedScopes, - trustedIps, - expiresIn: expiresIn === "" ? undefined : Number(expiresIn), - accessTokenTTL: Number(accessTokenTTL), - encryptedKey: ciphertext, - nonce, - isRefreshTokenRotationEnabled - }); - - const downloadData = { - public_key: publicKey, - private_key: privateKey, - refresh_token: refreshToken - }; - - const serviceTokenJSON = JSON.stringify(downloadData, null, 2); - setNewServiceTokenJSON(serviceTokenJSON); - - const blob = new Blob([serviceTokenJSON], { type: "application/json" }); - const href = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = href; - link.download = `infisical_${name}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - - createNotification({ - text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`, - type: "success" - }); - - reset(); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`, - type: "error" - }); - } - } - - const hasServiceTokenJSON = Boolean(newServiceTokenJSON); - - return ( - { - handlePopUpToggle("serviceTokenV3", isOpen); - reset(); - setNewServiceTokenJSON(""); - }} - > - - {!hasServiceTokenJSON ? ( -
- - -
- General - Advanced -
-
- - - ( - - - - )} - /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
- ( - - - - )} - /> -
-
- -
- {tokenTrustedIps.map(({ id }, index) => ( -
- { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} - /> - { - if (subscription?.ipAllowlisting) { - removeTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
- ( - - - - )} - /> -
- ( - onChange(isChecked)} - isChecked={value} - > - Refresh Token Rotation - - )} - /> -

When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.

-
-
-
-
-
- - -
-
- ) : ( -
-

{newServiceTokenJSON}

- - - - Click to copy - - -
- )} - handlePopUpToggle("upgradePlan", isOpen)} - text="You can use IP allowlisting if you switch to Infisical's Pro plan." - /> -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx deleted file mode 100644 index 3b00202f9..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; -import { withProjectPermission } from "@app/hoc"; -import { - useDeleteServiceTokenV3 -} from "@app/hooks/api"; -import { usePopUp } from "@app/hooks/usePopUp"; - -import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal"; -import { ServiceTokenV3Table } from "./ServiceTokenV3Table"; - -export const ServiceTokenV3Section = withProjectPermission( - () => { - const { createNotification } = useNotificationContext(); - const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "serviceTokenV3", - "deleteServiceTokenV3", - "upgradePlan" - ] as const); - - const onDeleteServiceTokenDataSubmit = async (serviceTokenDataId: string) => { - try { - await deleteMutateAsync({ - serviceTokenDataId - }); - createNotification({ - text: "Successfully deleted service token v3", - type: "success" - }); - - handlePopUpClose("deleteServiceTokenV3"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete service token v3", - type: "error" - }); - } - } - - return ( -
-
-

- Service Tokens V3 (Beta) -

- - {(isAllowed) => ( - - )} - -
- - - handlePopUpToggle("deleteServiceTokenV3", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onDeleteServiceTokenDataSubmit( - (popUp?.deleteServiceTokenV3?.data as { serviceTokenDataId: string })?.serviceTokenDataId - ) - } - /> -
- ); - }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens } -); \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx deleted file mode 100644 index d41a0bd82..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { format } from "date-fns"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - EmptyState, - IconButton, - Switch, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context"; -import { - useGetWorkspaceServiceTokenDataV3, - useUpdateServiceTokenV3 -} from "@app/hooks/api"; -import { Permission } from "@app/hooks/api/serviceTokens/enums" -import { ServiceTokenV3Scope, ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types" -import { UsePopUpState } from "@app/hooks/usePopUp"; - -type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>, - data?: { - serviceTokenDataId?: string; - name?: string; - scopes?: ServiceTokenV3Scope[]; - trustedIps?: ServiceTokenV3TrustedIp[]; - accessTokenTTL?: number; - isRefreshTokenRotationEnabled?: boolean; - } - ) => void; - }; - -export const ServiceTokenV3Table = ({ - handlePopUpOpen -}: Props) => { - const { createNotification } = useNotificationContext(); - const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || ""); - const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); - - const handleToggleServiceTokenDataStatus = async ({ - serviceTokenDataId, - isActive - }: { - serviceTokenDataId: string; - isActive: boolean; - }) => { - try { - await updateMutateAsync({ - serviceTokenDataId, - isActive - }); - - createNotification({ - text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`, - type: "success" - }); - } catch (err) { - console.log(err); - createNotification({ - text: `Failed to ${isActive ? "enable" : "disable"} service token v3`, - type: "error" - }); - } - } - - return ( - - - - - - - - - - - - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map(({ - _id, - name, - isActive, - scopes, - trustedIps, - createdAt, - expiresAt, - accessTokenTTL, - isRefreshTokenRotationEnabled - }) => { - return ( - - - - - - - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
NameStatusScopesTrusted IPsAccess Token TTLCreated AtValid Until -
{name} - - {(isAllowed) => ( - handleToggleServiceTokenDataStatus({ - serviceTokenDataId: _id, - isActive: value - })} - isChecked={isActive} - isDisabled={!isAllowed} - > -

{isActive ? "Active" : "Inactive"}

-
- )} -
-
- {scopes.map((scope) => { - let permissionText = "read" - if ( - scope.permissions.includes(Permission.WRITE) && - scope.permissions.includes(Permission.READ) - ) { - permissionText = "readWrite"; - } - - return ( -

- - {permissionText} - - {` @${scope.environment} - ${scope.secretPath}`} -

- ); - })} -
- {trustedIps.map(({ - _id: trustedIpId, - ipAddress, - prefix - }) => { - return ( -

- {`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`} -

- ); - })} -
{accessTokenTTL}{format(new Date(createdAt), "yyyy-MM-dd")}{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"} - - {(isAllowed) => ( - { - handlePopUpOpen("serviceTokenV3", { - serviceTokenDataId: _id, - name, - scopes, - trustedIps, - accessTokenTTL, - isRefreshTokenRotationEnabled - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - isDisabled={!isAllowed} - > - - - )} - - - {(isAllowed) => ( - { - handlePopUpOpen("deleteServiceTokenV3", { - serviceTokenDataId: _id, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - -
- -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/index.tsx deleted file mode 100644 index b6abc117c..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ServiceTokenV3Section } from "./ServiceTokenV3Section"; \ No newline at end of file