From 4683dc786992a4ef6151b89a47e17678200ea28b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 26 Sep 2023 10:56:08 +0100 Subject: [PATCH] Scope switch cases into blocks for secrets v3 --- .../src/controllers/v3/secretsController.ts | 503 ++++++++++++------ .../v3/serviceTokenDataController.ts | 11 +- backend/src/helpers/auth.ts | 34 +- backend/src/helpers/secrets.ts | 47 +- backend/src/models/serviceTokenDataV3.ts | 8 +- backend/src/routes/v3/secrets.ts | 20 +- backend/src/utils/auth.ts | 12 +- backend/src/validation/index.ts | 2 +- backend/src/validation/secrets.ts | 2 +- backend/src/validation/serviceTokenDataV3.ts | 96 ++++ backend/src/validation/serviceTokenV3.ts | 46 -- backend/src/validation/workspace.ts | 2 +- .../src/hooks/api/auditLogs/constants.tsx | 3 + frontend/src/hooks/api/auditLogs/enums.tsx | 7 +- frontend/src/hooks/api/auditLogs/types.tsx | 45 +- .../AuditLogsPage/components/LogsTable.tsx | 4 +- .../AuditLogsPage/components/LogsTableRow.tsx | 18 + .../AddServiceTokenV3Modal.tsx | 2 +- .../ServiceTokenV3Section.tsx | 2 +- 19 files changed, 620 insertions(+), 244 deletions(-) create mode 100644 backend/src/validation/serviceTokenDataV3.ts delete mode 100644 backend/src/validation/serviceTokenV3.ts diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index a528436af..66bfa1882 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -3,10 +3,22 @@ import { Types } from "mongoose"; import { EventService, SecretService } from "../../services"; import { eventPushSecrets } from "../../events"; import { BotService } from "../../services"; -import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets"; +import { + containsGlobPatterns, + isValidScope, + isValidScopeV3, + repackageSecretToRaw +} from "../../helpers/secrets"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { getAllImportedSecrets } from "../../services/SecretImportService"; -import { Folder, IServiceTokenData } from "../../models"; +import { + Folder, + IServiceTokenData, + IServiceTokenDataV3 +} from "../../models"; +import { + Permission +} from "../../models/serviceTokenDataV3"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; import { validateRequest } from "../../helpers/validation"; @@ -17,8 +29,12 @@ import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError, subject } from "@casl/ability"; -import { validateServiceTokenDataClientForWorkspace } from "../../validation"; +import { + validateServiceTokenDataClientForWorkspace, + validateServiceTokenDataV3ClientForWorkspace +} from "../../validation"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; +import { ActorType } from "../../ee/models"; /** * Return secrets for workspace with id [workspaceId] and environment @@ -59,30 +75,52 @@ export const getSecretsRaw = async (req: Request, res: Response) => { throw BadRequestError({ message: "Missing environment or workspace id" }); let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - permissionCheckFn = (env: string, secPath: string) => - permission.can( + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: env, - secretPath: secPath - }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_READ_SECRETS] - }); - permissionCheckFn = (env: string, secPath: string) => - isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath); + permissionCheckFn = (env: string, secPath: string) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: env, + secretPath: secPath + }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + permissionCheckFn = (env: string, secPath: string) => + isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + }); + permissionCheckFn = (env: string, secPath: string) => + isValidScopeV3({ + authPayload: req.authData.authPayload as IServiceTokenDataV3, + environment: env, + secretPath: secPath, + acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + }); + break; + } } const secrets = await SecretService.getSecrets({ @@ -150,20 +188,35 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.GetSecretByNameRawV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_READ_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ] + }); + break; + } } const secret = await SecretService.getSecret({ @@ -198,20 +251,35 @@ export const createSecretRaw = async (req: Request, res: Response) => { body: { secretPath, environment, workspaceId, type, secretValue, secretComment } } = await validateRequest(reqValidator.CreateSecretRawV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ_WRITE] + }); + break; + } } const key = await BotService.getWorkspaceKeyWithBot({ @@ -281,20 +349,35 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { body: { secretValue, environment, secretPath, type, workspaceId } } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ_WRITE] + }); + break; + } } const key = await BotService.getWorkspaceKeyWithBot({ @@ -345,20 +428,35 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { body: { environment, secretPath, type, workspaceId } } = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ_WRITE] + }); + break; + } } const { secret } = await SecretService.deleteSecret({ @@ -414,30 +512,53 @@ export const getSecrets = async (req: Request, res: Response) => { } let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - permissionCheckFn = (env: string, secPath: string) => - permission.can( + + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: env, - secretPath: secPath - }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_READ_SECRETS] - }); - permissionCheckFn = (env: string, secPath: string) => - isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath); + permissionCheckFn = (env: string, secPath: string) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: env, + secretPath: secPath + }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + permissionCheckFn = (env: string, secPath: string) => + isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + }); + permissionCheckFn = (env: string, secPath: string) => + isValidScopeV3({ + authPayload: req.authData.authPayload as IServiceTokenDataV3, + environment: env, + secretPath: secPath, + acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + }); + break; + } } const secrets = await SecretService.getSecrets({ @@ -486,21 +607,36 @@ export const getSecretByName = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId, type }, params: { secretName } } = await validateRequest(reqValidator.GetSecretByNameV3, req); - - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_READ_SECRETS] - }); + + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ] + }); + break; + } } const secret = await SecretService.getSecret({ @@ -543,20 +679,35 @@ export const createSecret = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.CreateSecretV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ_WRITE] + }); + break; + } } const secret = await SecretService.createSecret({ @@ -613,22 +764,37 @@ export const updateSecretByName = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.UpdateSecretByNameV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ_WRITE] + }); + break; + } } - + const secret = await SecretService.updateSecret({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -665,20 +831,35 @@ export const deleteSecretByName = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.DeleteSecretByNameV3, req); - if (req.user?._id) { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } else { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); + switch (req.authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + break; + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + break; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + acceptedPermissions: [Permission.READ_WRITE] + }); + break; + } } const { secret } = await SecretService.deleteSecret({ diff --git a/backend/src/controllers/v3/serviceTokenDataController.ts b/backend/src/controllers/v3/serviceTokenDataController.ts index fe7d9fc9d..fdc68eddb 100644 --- a/backend/src/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/controllers/v3/serviceTokenDataController.ts @@ -8,10 +8,11 @@ import { Scope } from "../../models/serviceTokenDataV3"; import { + ActorType, EventType } from "../../ee/models"; import { validateRequest } from "../../helpers/validation"; -import * as reqValidator from "../../validation/serviceTokenV3"; +import * as reqValidator from "../../validation/serviceTokenDataV3"; import { createToken } from "../../helpers/auth"; import { ProjectPermissionActions, @@ -52,9 +53,15 @@ export const createServiceTokenData = async (req: Request, res: Response) => { expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); } + let user; + if (req.authData.actor.type === ActorType.USER) { + user = req.authData.authPayload._id; + } + const isActive = false; const serviceTokenData = await new ServiceTokenDataV3({ name, + user, workspace: new Types.ObjectId(workspaceId), publicKey, scopes, @@ -161,7 +168,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { { type: EventType.UPDATE_SERVICE_TOKEN_V3, metadata: { - name, + name: serviceTokenData.name, isActive, scopes: scopes as Array, expiresAt diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 0345b5c18..55d8eeb53 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -70,6 +70,7 @@ export const validateAuthMode = ({ if (typeof authHeader === "string") { // case: treat request authentication type as via Authorization header (i.e. either JWT or service token) const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null] + if (tokenType === null) throw BadRequestError({ message: "Missing Authorization Header in the request header." }); if (tokenType.toLowerCase() !== "bearer") @@ -77,18 +78,21 @@ export const validateAuthMode = ({ if (tokenValue === null) throw BadRequestError({ message: "Missing Authorization Body in the request header." }); - switch (tokenValue.split(".", 1)[0]) { + const parts = tokenValue.split("."); + + switch (parts[0]) { case "st": authMode = AuthMode.SERVICE_TOKEN; + authTokenValue = tokenValue; break; case "proj_token": authMode = AuthMode.SERVICE_TOKEN_V3; + authTokenValue = parts.slice(1).join('.'); break; default: authMode = AuthMode.JWT; + authTokenValue = tokenValue; } - - authTokenValue = tokenValue; } if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." }); @@ -235,29 +239,39 @@ export const getAuthSTDPayload = async ({ authTokenValue: string; }): Promise => { const decodedToken = ( - jwt.verify(authTokenValue, "hello") // TODO: change this + jwt.verify(authTokenValue, "hello") // TODO: replace with real secret ); - // perhaps turn this one into a find one and update call? const serviceTokenData = await ServiceTokenDataV3.findOne({ - _id: new Types.ObjectId(decodedToken.serviceTokenDataId), + _id: new Types.ObjectId(decodedToken._id), + isActive: true }); - + if (!serviceTokenData) { throw UnauthorizedRequestError({ message: "Failed to authenticate" // standardize auth error messages }); } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { // case: service token expired - await ServiceTokenDataV3.findByIdAndDelete(serviceTokenData._id); + // TODO: test expired token + await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + isActive: false + }, + { + new: true + } + ); + throw UnauthorizedRequestError({ message: "Failed to authenticate", }); } - + return { actor: { - type: ActorType.SERVICE_V3, // should this be servicev3 bc the shape of it is different? + type: ActorType.SERVICE_V3, metadata: { serviceId: serviceTokenData._id.toString(), name: serviceTokenData.name diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 3799dc1e7..09c063b6f 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -10,11 +10,16 @@ import { Folder, ISecret, IServiceTokenData, + IServiceTokenDataV3, Secret, SecretBlindIndexData, ServiceTokenData, TFolderRootSchema } from "../models"; +import { + Scope, + Permission +} from "../models/serviceTokenDataV3"; import { EventType, SecretVersion } from "../ee/models"; import { BadRequestError, @@ -49,10 +54,50 @@ import { getFolderByPath, getFolderIdFromServiceToken } from "../services/Folder import picomatch from "picomatch"; import path from "path"; +/** + * Validate scope for service token v3 + * @param authPayload + * @param environment + * @param secretPath + * @returns + */ +export const isValidScopeV3 = ({ + authPayload, + environment, + secretPath, + acceptedPermissions +}: { + authPayload: IServiceTokenDataV3, + environment: string, + secretPath: string, + acceptedPermissions: Permission[] +}) => { + const { scopes } = authPayload; + + const validScope = scopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + + if (validScope && !acceptedPermissions.includes(validScope.permission)) { + return false; + } + + return Boolean(validScope); +} + +/** + * Validate scope for service token v2 + * @param authPayload + * @param environment + * @param secretPath + * @returns + */ export const isValidScope = ( authPayload: IServiceTokenData, environment: string, - secretPath: string + secretPath: string, ) => { const { scopes: tkScopes } = authPayload; const validScope = tkScopes.find( diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index fa0aca079..fe09a5a2e 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -1,6 +1,6 @@ import { Document, Schema, Types, model } from "mongoose"; -enum Permission { +export enum Permission { READ = "read", READ_WRITE = "readWrite" } @@ -15,6 +15,7 @@ export interface IServiceTokenDataV3 extends Document { _id: Types.ObjectId; name: string; workspace: Types.ObjectId; + user: Types.ObjectId; publicKey: string; isActive: boolean; lastUsed?: Date; @@ -33,6 +34,11 @@ const serviceTokenDataV3Schema = new Schema( ref: "Workspace", required: true }, + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true + }, publicKey: { type: String, required: true diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 1765c2e33..95c3ba8f9 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -13,7 +13,7 @@ import { router.get( "/raw", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), secretsController.getSecretsRaw ); @@ -21,7 +21,7 @@ router.get( router.get( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -35,7 +35,7 @@ router.get( router.post( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -49,7 +49,7 @@ router.post( router.patch( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -63,7 +63,7 @@ router.patch( router.delete( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -77,7 +77,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -88,7 +88,7 @@ router.get( router.post( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -99,7 +99,7 @@ router.post( router.get( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -110,7 +110,7 @@ router.get( router.patch( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -121,7 +121,7 @@ router.patch( router.delete( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 3a04639a6..45e8e47f5 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -8,6 +8,7 @@ import { Organization, ServiceAccount, ServiceTokenData, + ServiceTokenDataV3, User } from "../models"; import { createToken } from "../helpers/auth"; @@ -49,6 +50,10 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => { if (authData.authPayload instanceof ServiceTokenData) { return { serviceTokenDataId: authData.authPayload._id }; } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { serviceTokenDataId: authData.authPayload._id }; + } }; /** @@ -57,7 +62,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => { * @returns */ const getAuthDataPayloadUserObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { return { user: authData.authPayload._id }; } @@ -67,7 +71,11 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => { } if (authData.authPayload instanceof ServiceTokenData) { - return { user: authData.authPayload.user };0 + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { user: authData.authPayload.user }; } } diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 9f20d6741..a823f8095 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -9,4 +9,4 @@ export * from "./organization"; export * from "./secrets"; export * from "./serviceAccount"; export * from "./serviceTokenData"; -export * from "./serviceTokenV3"; +export * from "./serviceTokenDataV3"; diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 28e7f469d..a8b17ead7 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -254,7 +254,7 @@ export const CreateSecretRawV3 = z.object({ environment: z.string().trim(), secretPath: z.string().trim().default("/"), secretValue: z.string().trim(), - secretComment: z.string().trim(), + secretComment: z.string().trim().optional().default(""), type: z.enum([SECRET_SHARED, SECRET_PERSONAL]) }), params: z.object({ diff --git a/backend/src/validation/serviceTokenDataV3.ts b/backend/src/validation/serviceTokenDataV3.ts new file mode 100644 index 000000000..2aa5c476a --- /dev/null +++ b/backend/src/validation/serviceTokenDataV3.ts @@ -0,0 +1,96 @@ +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"; + +/** + * 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 ({ + serviceTokenData, + workspaceId, + environment, + secretPath = "/", + acceptedPermissions +}: { + serviceTokenData: IServiceTokenDataV3; + workspaceId: Types.ObjectId; + environment?: string; + secretPath?: string; + acceptedPermissions: Permission[]; +}) => { + + 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, + acceptedPermissions + }); + + if (!isValid) throw UnauthorizedRequestError({ + message: "Failed service token authorization for the given workspace" + }); + } +}; + +export const CreateServiceTokenV3 = z.object({ + body: z.object({ + name: z.string().trim(), + workspaceId: z.string().trim(), + publicKey: z.string().trim(), + scopes: z + .object({ + permission: z.enum(["read", "readWrite"]), + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + .min(1), + expiresIn: z.number().optional(), + encryptedKey: z.string().trim(), + nonce: z.string().trim() + }) +}); + +export const UpdateServiceTokenV3 = z.object({ + params: z.object({ + serviceTokenDataId: z.string() + }), + body: z.object({ + name: z.string().trim().optional(), + isActive: z.boolean().optional(), + scopes: z + .object({ + permission: z.enum(["read", "readWrite"]), + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + .min(1) + .optional(), + expiresIn: z.number().optional() + }), +}); + +export const DeleteServiceTokenV3 = z.object({ + params: z.object({ + serviceTokenDataId: z.string() + }), +}); \ No newline at end of file diff --git a/backend/src/validation/serviceTokenV3.ts b/backend/src/validation/serviceTokenV3.ts deleted file mode 100644 index 377bba287..000000000 --- a/backend/src/validation/serviceTokenV3.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from "zod"; - -export const CreateServiceTokenV3 = z.object({ - body: z.object({ - name: z.string().trim(), - workspaceId: z.string().trim(), - publicKey: z.string().trim(), - scopes: z - .object({ - permission: z.enum(["read", "readWrite"]), - environment: z.string().trim(), - secretPath: z.string().trim() - }) - .array() - .min(1), - expiresIn: z.number().optional(), - encryptedKey: z.string().trim(), - nonce: z.string().trim() - }) -}); - -export const UpdateServiceTokenV3 = z.object({ - params: z.object({ - serviceTokenDataId: z.string() - }), - body: z.object({ - name: z.string().trim().optional(), - isActive: z.boolean().optional(), - scopes: z - .object({ - permission: z.enum(["read", "readWrite"]), - environment: z.string().trim(), - secretPath: z.string().trim() - }) - .array() - .min(1) - .optional(), - expiresIn: z.number().optional() - }), -}); - -export const DeleteServiceTokenV3 = z.object({ - params: z.object({ - serviceTokenDataId: z.string() - }), -}); \ No newline at end of file diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index ade27f136..c44b234e4 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -57,7 +57,7 @@ export const validateClientForWorkspace = async ({ environment, requiredPermissions }); - break; + return { membership, workspace}; case ActorType.SERVICE_V3: throw UnauthorizedRequestError({ message: "Failed service token authorization for organization" diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index cfdee0a10..9d31f812d 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -16,6 +16,9 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.DELETE_TRUSTED_IP]: "Delete trusted IP", [EventType.CREATE_SERVICE_TOKEN]: "Create service token", [EventType.DELETE_SERVICE_TOKEN]: "Delete service token", + [EventType.CREATE_SERVICE_TOKEN_V3]: "Create (new) service token", + [EventType.UPDATE_SERVICE_TOKEN_V3]: "Update (new) service token", + [EventType.DELETE_SERVICE_TOKEN_V3]: "Delete (new) service token", [EventType.CREATE_ENVIRONMENT]: "Create environment", [EventType.UPDATE_ENVIRONMENT]: "Update environment", [EventType.DELETE_ENVIRONMENT]: "Delete environment", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index d19876525..765b2b941 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -24,8 +24,11 @@ export enum EventType { ADD_TRUSTED_IP = "add-trusted-ip", UPDATE_TRUSTED_IP = "update-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip", - CREATE_SERVICE_TOKEN = "create-service-token", - DELETE_SERVICE_TOKEN = "delete-service-token", + CREATE_SERVICE_TOKEN = "create-service-token", // v2 + DELETE_SERVICE_TOKEN = "delete-service-token", // v2 + CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3 + UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3 + DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3 CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 40488f196..8b9767295 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -4,6 +4,17 @@ 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; @@ -14,7 +25,6 @@ interface ServiceActorMetadata { name: string; } - interface UserActor { type: ActorType.USER; metadata: UserActorMetadata; @@ -190,6 +200,36 @@ interface DeleteServiceTokenEvent { } } +interface CreateServiceTokenV3Event { + type: EventType.CREATE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + expiresAt?: Date; + } +} + +interface UpdateServiceTokenV3Event { + type: EventType.UPDATE_SERVICE_TOKEN_V3; + metadata: { + name?: string; + isActive?: boolean; + scopes?: Array; + expiresAt?: Date; + } +} + +interface DeleteServiceTokenV3Event { + type: EventType.DELETE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + expiresAt?: Date; + } +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -387,6 +427,9 @@ export type Event = | DeleteTrustedIPEvent | CreateServiceTokenEvent | DeleteServiceTokenEvent + | CreateServiceTokenV3Event + | UpdateServiceTokenV3Event + | DeleteServiceTokenV3Event | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsTable.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsTable.tsx index 6c2bdc710..f6d38bf7c 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsTable.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsTable.tsx @@ -93,6 +93,4 @@ export const LogsTable = ({ )} ); -} - -// TODO: retrieve count \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx index 0520d73ba..52cd88cc1 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx @@ -160,6 +160,24 @@ export const LogsTableRow = ({

{`Name: ${event.metadata.name}`}

); + case EventType.CREATE_SERVICE_TOKEN_V3: + return ( + +

{`Name: ${event.metadata.name}`}

+ + ); + case EventType.UPDATE_SERVICE_TOKEN_V3: + return ( + +

{`Name: ${event.metadata.name}`}

+ + ); + case EventType.DELETE_SERVICE_TOKEN_V3: + return ( + +

{`Name: ${event.metadata.name}`}

+ + ); case EventType.CREATE_ENVIRONMENT: return ( diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index 1a88d1207..f0e03217b 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -420,7 +420,7 @@ export const AddServiceTokenV3Modal = ({ isLoading={isSubmitting} isDisabled={isSubmitting} > - Create + {popUp?.serviceTokenV3?.data ? "Update" : "Create"} )}