diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 772ad8bde..a4669ffa4 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -1,29 +1,25 @@ -import { Request, Response } from 'express'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; +import { Request, Response } from "express"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; +import { User, ServiceAccount, ServiceTokenData } from "../../models"; +import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions"; import { - User, - ServiceAccount, - ServiceTokenData -} from '../../models'; -import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; -import { - PERMISSION_READ_SECRETS, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN -} from '../../variables'; -import { getSaltRounds } from '../../config'; -import { BadRequestError } from '../../utils/errors'; + PERMISSION_READ_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, +} from "../../variables"; +import { getSaltRounds } from "../../config"; +import { BadRequestError } from "../../utils/errors"; /** * Return service token data associated with service token on request - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getServiceTokenData = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return Infisical Token data' #swagger.description = 'Return Infisical Token data' @@ -49,98 +45,110 @@ export const getServiceTokenData = async (req: Request, res: Response) => { } */ - if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({ - message: 'Failed accepted client validation for service token data' + if (!(req.authData.authPayload instanceof ServiceTokenData)) + throw BadRequestError({ + message: "Failed accepted client validation for service token data", }); - const serviceTokenData = await ServiceTokenData - .findById(req.authData.authPayload._id) - .select('+encryptedKey +iv +tag') - .populate('user'); + const serviceTokenData = await ServiceTokenData.findById( + req.authData.authPayload._id + ) + .select("+encryptedKey +iv +tag") + .populate("user"); - return res.status(200).json(serviceTokenData); -} + return res.status(200).json(serviceTokenData); +}; /** * Create new service token data for workspace with id [workspaceId] and * environment [environment]. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createServiceTokenData = async (req: Request, res: Response) => { - let serviceTokenData; + let serviceTokenData; - const { - name, - workspaceId, - environment, - encryptedKey, - iv, - tag, - expiresIn, - permissions - } = req.body; + const { + name, + workspaceId, + environment, + encryptedKey, + iv, + tag, + expiresIn, + secretPath, + permissions, + } = req.body; - const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, await getSaltRounds()); + const secret = crypto.randomBytes(16).toString("hex"); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); - let expiresAt; - if (expiresIn) { - expiresAt = new Date() - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - } + let expiresAt; + if (expiresIn) { + expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } - let user, serviceAccount; - - if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { - user = req.authData.authPayload._id; - } + let user, serviceAccount; - if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) { - serviceAccount = req.authData.authPayload._id; - } - - serviceTokenData = await new ServiceTokenData({ - name, - workspace: workspaceId, - environment, - user, - serviceAccount, - lastUsed: new Date(), - expiresAt, - secretHash, - encryptedKey, - iv, - tag, - permissions - }).save(); + if ( + req.authData.authMode === AUTH_MODE_JWT && + req.authData.authPayload instanceof User + ) { + user = req.authData.authPayload._id; + } - // return service token data without sensitive data - serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); + if ( + req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && + req.authData.authPayload instanceof ServiceAccount + ) { + serviceAccount = req.authData.authPayload._id; + } - if (!serviceTokenData) throw new Error('Failed to find service token data'); + serviceTokenData = await new ServiceTokenData({ + name, + workspace: workspaceId, + environment, + user, + serviceAccount, + lastUsed: new Date(), + expiresAt, + secretHash, + encryptedKey, + iv, + tag, + secretPath, + permissions, + }).save(); - const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); - return res.status(200).send({ - serviceToken, - serviceTokenData - }); -} + if (!serviceTokenData) throw new Error("Failed to find service token data"); + + const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + + return res.status(200).send({ + serviceToken, + serviceTokenData, + }); +}; /** * Delete service token data with id [serviceTokenDataId]. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteServiceTokenData = async (req: Request, res: Response) => { - const { serviceTokenDataId } = req.params; + const { serviceTokenDataId } = req.params; - const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + const serviceTokenData = await ServiceTokenData.findByIdAndDelete( + serviceTokenDataId + ); - return res.status(200).send({ - serviceTokenData - }); -} + return res.status(200).send({ + serviceTokenData, + }); +}; diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index dac41b53d..c3853d3c2 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -1,183 +1,184 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { - SecretService, - TelemetryService, - EventService -} from '../../services'; -import { eventPushSecrets } from '../../events'; -import { getAuthDataPayloadIdObj } from '../../utils/auth'; -import { BadRequestError } from '../../utils/errors'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { SecretService, EventService } from "../../services"; +import { eventPushSecrets } from "../../events"; /** * Get secrets for workspace with id [workspaceId] and environment * [environment] - * @param req - * @param res + * @param req + * @param res */ export const getSecrets = async (req: Request, res: Response) => { - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; + const secretPath = req.query.secretPath as string; - const secrets = await SecretService.getSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment, - authData: req.authData - }); + const secrets = await SecretService.getSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + authData: req.authData, + }); - return res.status(200).send({ - secrets - }); -} + return res.status(200).send({ + secrets, + }); +}; /** * Get secret with name [secretName] - * @param req - * @param res + * @param req + * @param res */ export const getSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const type = req.query.type as 'shared' | 'personal' | undefined; + const { secretName } = req.params; + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; + const secretPath = req.query.secretPath as string; + const type = req.query.type as "shared" | "personal" | undefined; - const secret = await SecretService.getSecret({ - secretName, - workspaceId: new Types.ObjectId(workspaceId), - environment, - type, - authData: req.authData - }); - - return res.status(200).send({ - secret - }); -} + const secret = await SecretService.getSecret({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + secretPath, + authData: req.authData, + }); + + return res.status(200).send({ + secret, + }); +}; /** * Create secret with name [secretName] - * @param req - * @param res + * @param req + * @param res */ export const createSecret = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } = req.body; - - const secret = await SecretService.createSecret({ - secretName, - workspaceId: new Types.ObjectId(workspaceId), - environment, - type, - authData: req.authData, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - ...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } : {}) - }); + const { secretName } = req.params; + const { + workspaceId, + environment, + type, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretPath = "/", + } = req.body; - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment - }) - }); + const secret = await SecretService.createSecret({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + authData: req.authData, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretPath, + ...(secretCommentCiphertext && secretCommentIV && secretCommentTag + ? { + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + } + : {}), + }); - const secretWithoutBlindIndex = secret.toObject(); - delete secretWithoutBlindIndex.secretBlindIndex; - - return res.status(200).send({ - secret: secretWithoutBlindIndex - }); -} + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + }), + }); + + const secretWithoutBlindIndex = secret.toObject(); + delete secretWithoutBlindIndex.secretBlindIndex; + + return res.status(200).send({ + secret: secretWithoutBlindIndex, + }); +}; /** * Update secret with name [secretName] * @param req - * @param res + * @param res */ export const updateSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretValueCiphertext, - secretValueIV, - secretValueTag - } = req.body; + const { secretName } = req.params; + const { + workspaceId, + environment, + type, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretPath = "/", + } = req.body; - const secret = await SecretService.updateSecret({ - secretName, - workspaceId, - environment, - type, - authData: req.authData, - secretValueCiphertext, - secretValueIV, - secretValueTag - }); - - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment - }) - }); + const secret = await SecretService.updateSecret({ + secretName, + workspaceId, + environment, + type, + authData: req.authData, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretPath, + }); - return res.status(200).send({ - secret - }); -} + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + }), + }); + + return res.status(200).send({ + secret, + }); +}; /** * Delete secret with name [secretName] - * @param req - * @param res + * @param req + * @param res */ export const deleteSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { - workspaceId, - environment, - type - } = req.body; - - const { secret, secrets } = await SecretService.deleteSecret({ - secretName, - workspaceId, - environment, - type, - authData: req.authData - }); + const { secretName } = req.params; + const { workspaceId, environment, type, secretPath = "/" } = req.body; - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment - }) - }); + const { secret } = await SecretService.deleteSecret({ + secretName, + workspaceId, + environment, + type, + authData: req.authData, + secretPath, + }); - return res.status(200).send({ - secret - }); -} \ No newline at end of file + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + }), + }); + + return res.status(200).send({ + secret, + }); +}; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 1c58119b1..495d11119 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -1,19 +1,24 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { CreateSecretParams, GetSecretsParams, GetSecretParams, UpdateSecretParams, DeleteSecretParams, -} from '../interfaces/services/SecretService'; -import { Secret, ISecret, SecretBlindIndexData } from '../models'; -import { SecretVersion } from '../ee/models'; +} from "../interfaces/services/SecretService"; +import { + Secret, + ISecret, + SecretBlindIndexData, + ServiceTokenData, +} from "../models"; +import { SecretVersion } from "../ee/models"; import { BadRequestError, SecretNotFoundError, SecretBlindIndexDataNotFoundError, InternalServerError, -} from '../utils/errors'; +} from "../utils/errors"; import { SECRET_PERSONAL, SECRET_SHARED, @@ -24,20 +29,44 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64, -} from '../variables'; -import crypto from 'crypto'; -import * as argon2 from 'argon2'; +} from "../variables"; +import crypto from "crypto"; +import * as argon2 from "argon2"; import { encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8, -} from '../utils/crypto'; -import { getEncryptionKey, client, getRootEncryptionKey } from '../config'; -import { TelemetryService } from '../services'; -import { EESecretService, EELogService } from '../ee/services'; +} from "../utils/crypto"; +import { getEncryptionKey, client, getRootEncryptionKey } from "../config"; +import { TelemetryService } from "../services"; +import { EESecretService, EELogService } from "../ee/services"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj, -} from '../utils/auth'; +} from "../utils/auth"; +import Folder from "../models/folder"; +import { getFolderByPath } from "../services/FolderService"; + +export const getFolderIdFromServiceToken = async ( + workspaceId: Types.ObjectId | string, + environment: string, + secretPath: string +) => { + const folders = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + + if (!folders) { + if (secretPath !== "/") throw new Error("Invalid path. Folders not found"); + } else { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw new Error("Folder not found"); + } + return folder.id; + } + return "root"; +}; /** * Create secret blind index data containing encrypted blind index [salt] @@ -46,12 +75,12 @@ import { * @param {Types.ObjectId} obj.workspaceId */ export const createSecretBlindIndexDataHelper = async ({ - workspaceId + workspaceId, }: { workspaceId: Types.ObjectId; }) => { // initialize random blind index salt for workspace - const salt = crypto.randomBytes(16).toString('base64'); + const salt = crypto.randomBytes(16).toString("base64"); const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); @@ -99,7 +128,7 @@ export const createSecretBlindIndexDataHelper = async ({ * @returns */ export const getSecretBlindIndexSaltHelper = async ({ - workspaceId + workspaceId, }: { workspaceId: Types.ObjectId; }) => { @@ -108,7 +137,7 @@ export const getSecretBlindIndexSaltHelper = async ({ const secretBlindIndexData = await SecretBlindIndexData.findOne({ workspace: workspaceId, - }).select('+algorithm +keyEncoding'); + }).select("+algorithm +keyEncoding"); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); @@ -136,7 +165,7 @@ export const getSecretBlindIndexSaltHelper = async ({ } throw InternalServerError({ - message: 'Failed to obtain workspace salt needed for secret blind indexing', + message: "Failed to obtain workspace salt needed for secret blind indexing", }); }; @@ -148,8 +177,8 @@ export const getSecretBlindIndexSaltHelper = async ({ * @param {String} obj.salt - base64-salt */ export const generateSecretBlindIndexWithSaltHelper = async ({ - secretName, - salt + secretName, + salt, }: { secretName: string; salt: string; @@ -158,14 +187,14 @@ export const generateSecretBlindIndexWithSaltHelper = async ({ const secretBlindIndex = ( await argon2.hash(secretName, { type: argon2.argon2id, - salt: Buffer.from(salt, 'base64'), + salt: Buffer.from(salt, "base64"), saltLength: 16, // default 16 bytes memoryCost: 65536, // default pool of 64 MiB per thread. hashLength: 32, parallelism: 1, raw: true, }) - ).toString('base64'); + ).toString("base64"); return secretBlindIndex; }; @@ -178,8 +207,8 @@ export const generateSecretBlindIndexWithSaltHelper = async ({ * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to */ export const generateSecretBlindIndexHelper = async ({ - secretName, - workspaceId + secretName, + workspaceId, }: { secretName: string; workspaceId: Types.ObjectId; @@ -190,7 +219,7 @@ export const generateSecretBlindIndexHelper = async ({ const secretBlindIndexData = await SecretBlindIndexData.findOne({ workspace: workspaceId, - }).select('+algorithm +keyEncoding'); + }).select("+algorithm +keyEncoding"); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); @@ -231,9 +260,9 @@ export const generateSecretBlindIndexHelper = async ({ return secretBlindIndex; } - + throw InternalServerError({ - message: 'Failed to generate secret blind index' + message: "Failed to generate secret blind index", }); }; @@ -262,23 +291,37 @@ export const createSecretHelper = async ({ secretCommentCiphertext, secretCommentIV, secretCommentTag, - folderId, + secretPath = "/", }: CreateSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, workspaceId: new Types.ObjectId(workspaceId), }); + // if using service token filter towards the folderId by secretpath + if (authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + if (secretPath !== serviceTkScopedSecretPath) { + throw new Error("Folder Permission Denied"); + } + } + const folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); + const exists = await Secret.exists({ secretBlindIndex, workspace: new Types.ObjectId(workspaceId), + folder: folderId, type, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), }); if (exists) throw BadRequestError({ - message: 'Failed to create secret that already exists', + message: "Failed to create secret that already exists", }); if (type === SECRET_PERSONAL) { @@ -287,6 +330,7 @@ export const createSecretHelper = async ({ const exists = await Secret.exists({ secretBlindIndex, + folder: folderId, workspace: new Types.ObjectId(workspaceId), type: SECRET_SHARED, }); @@ -294,7 +338,7 @@ export const createSecretHelper = async ({ if (!exists) throw BadRequestError({ message: - 'Failed to create personal secret override for no corresponding shared secret', + "Failed to create personal secret override for no corresponding shared secret", }); } @@ -325,6 +369,7 @@ export const createSecretHelper = async ({ version: secret.version, workspace: secret.workspace, type, + folder: folderId, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), environment: secret.environment, isDeleted: false, @@ -372,7 +417,7 @@ export const createSecretHelper = async ({ if (postHogClient) { postHogClient.capture({ - event: 'secrets added', + event: "secrets added", distinctId: await TelemetryService.getDistinctId({ authData, }), @@ -380,6 +425,7 @@ export const createSecretHelper = async ({ numberOfSecrets: 1, environment, workspaceId, + folderId, channel: authData.authChannel, userAgent: authData.authUserAgent, }, @@ -398,16 +444,30 @@ export const createSecretHelper = async ({ * @returns */ export const getSecretsHelper = async ({ - workspaceId, - environment, - authData + workspaceId, + environment, + authData, + secretPath = "/", }: GetSecretsParams) => { let secrets: ISecret[] = []; + // if using service token filter towards the folderId by secretpath + if (authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + if (secretPath !== serviceTkScopedSecretPath) { + throw new Error("Folder Permission Denied"); + } + } + const folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); // get personal secrets first secrets = await Secret.find({ workspace: new Types.ObjectId(workspaceId), environment, + folder: folderId, type: SECRET_PERSONAL, ...getAuthDataPayloadUserObj(authData), }); @@ -417,6 +477,7 @@ export const getSecretsHelper = async ({ await Secret.find({ workspace: new Types.ObjectId(workspaceId), environment, + folder: folderId, type: SECRET_SHARED, secretBlindIndex: { $nin: secrets.map((secret) => secret.secretBlindIndex), @@ -445,7 +506,7 @@ export const getSecretsHelper = async ({ if (postHogClient) { postHogClient.capture({ - event: 'secrets pulled', + event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ authData, }), @@ -453,6 +514,7 @@ export const getSecretsHelper = async ({ numberOfSecrets: secrets.length, environment, workspaceId, + folderId, channel: authData.authChannel, userAgent: authData.authUserAgent, }, @@ -478,18 +540,32 @@ export const getSecretHelper = async ({ environment, type, authData, + secretPath = "/", }: GetSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, workspaceId: new Types.ObjectId(workspaceId), }); let secret: ISecret | null = null; + // if using service token filter towards the folderId by secretpath + if (authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + if (secretPath !== serviceTkScopedSecretPath) { + throw new Error("Folder Permission Denied"); + } + } + const folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); // try getting personal secret first (if exists) secret = await Secret.findOne({ secretBlindIndex, workspace: new Types.ObjectId(workspaceId), environment, + folder: folderId, type: type ?? SECRET_PERSONAL, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), }); @@ -501,6 +577,7 @@ export const getSecretHelper = async ({ secretBlindIndex, workspace: new Types.ObjectId(workspaceId), environment, + folder: folderId, type: SECRET_SHARED, }); } @@ -528,7 +605,7 @@ export const getSecretHelper = async ({ if (postHogClient) { postHogClient.capture({ - event: 'secrets pull', + event: "secrets pull", distinctId: await TelemetryService.getDistinctId({ authData, }), @@ -536,6 +613,7 @@ export const getSecretHelper = async ({ numberOfSecrets: 1, environment, workspaceId, + folderId, channel: authData.authChannel, userAgent: authData.authUserAgent, }, @@ -568,6 +646,7 @@ export const updateSecretHelper = async ({ secretValueCiphertext, secretValueIV, secretValueTag, + secretPath, }: UpdateSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, @@ -575,6 +654,18 @@ export const updateSecretHelper = async ({ }); let secret: ISecret | null = null; + // if using service token filter towards the folderId by secretpath + if (authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + if (secretPath !== serviceTkScopedSecretPath) { + throw new Error("Folder Permission Denied"); + } + } + const folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); if (type === SECRET_SHARED) { // case: update shared secret @@ -583,6 +674,7 @@ export const updateSecretHelper = async ({ secretBlindIndex, workspace: new Types.ObjectId(workspaceId), environment, + folder: folderId, type, }, { @@ -604,6 +696,7 @@ export const updateSecretHelper = async ({ workspace: new Types.ObjectId(workspaceId), environment, type, + folder: folderId, ...getAuthDataPayloadUserObj(authData), }, { @@ -624,6 +717,7 @@ export const updateSecretHelper = async ({ secret: secret._id, version: secret.version, workspace: secret.workspace, + folder: folderId, type, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), environment: secret.environment, @@ -672,7 +766,7 @@ export const updateSecretHelper = async ({ if (postHogClient) { postHogClient.capture({ - event: 'secrets modified', + event: "secrets modified", distinctId: await TelemetryService.getDistinctId({ authData, }), @@ -680,6 +774,7 @@ export const updateSecretHelper = async ({ numberOfSecrets: 1, environment, workspaceId, + folderId, channel: authData.authChannel, userAgent: authData.authUserAgent, }, @@ -705,12 +800,26 @@ export const deleteSecretHelper = async ({ environment, type, authData, + secretPath = "/", }: DeleteSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, workspaceId: new Types.ObjectId(workspaceId), }); + // if using service token filter towards the folderId by secretpath + if (authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + if (secretPath !== serviceTkScopedSecretPath) { + throw new Error("Folder Permission Denied"); + } + } + const folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); + let secrets: ISecret[] = []; let secret: ISecret | null = null; @@ -719,6 +828,7 @@ export const deleteSecretHelper = async ({ secretBlindIndex, workspaceId: new Types.ObjectId(workspaceId), environment, + folder: folderId, }); secret = await Secret.findOneAndDelete({ @@ -726,16 +836,19 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId), environment, type, + folder: folderId, }); await Secret.deleteMany({ secretBlindIndex, workspaceId: new Types.ObjectId(workspaceId), environment, + folder: folderId, }); } else { secret = await Secret.findOneAndDelete({ secretBlindIndex, + folder: folderId, workspaceId: new Types.ObjectId(workspaceId), environment, type, @@ -782,7 +895,7 @@ export const deleteSecretHelper = async ({ if (postHogClient) { postHogClient.capture({ - event: 'secrets deleted', + event: "secrets deleted", distinctId: await TelemetryService.getDistinctId({ authData, }), @@ -790,6 +903,7 @@ export const deleteSecretHelper = async ({ numberOfSecrets: secrets.length, environment, workspaceId, + folderId, channel: authData.authChannel, userAgent: authData.authUserAgent, }, @@ -800,4 +914,4 @@ export const deleteSecretHelper = async ({ secrets, secret, }; -}; \ No newline at end of file +}; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index 2efd53f4e..678c67711 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -5,7 +5,6 @@ export interface CreateSecretParams { secretName: string; workspaceId: Types.ObjectId; environment: string; - folderId?: string; type: "shared" | "personal"; authData: AuthData; secretKeyCiphertext: string; @@ -17,17 +16,20 @@ export interface CreateSecretParams { secretCommentCiphertext?: string; secretCommentIV?: string; secretCommentTag?: string; + secretPath: string; } export interface GetSecretsParams { workspaceId: Types.ObjectId; environment: string; + secretPath: string; authData: AuthData; } export interface GetSecretParams { secretName: string; workspaceId: Types.ObjectId; + secretPath: string; environment: string; type?: "shared" | "personal"; authData: AuthData; @@ -42,7 +44,7 @@ export interface UpdateSecretParams { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; - folderId?: string; + secretPath: string; } export interface DeleteSecretParams { @@ -51,4 +53,5 @@ export interface DeleteSecretParams { environment: string; type: "shared" | "personal"; authData: AuthData; + secretPath: string; } diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 36a63c094..3587a7ff4 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -1,79 +1,88 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Schema, model, Types, Document } from "mongoose"; export interface IServiceTokenData extends Document { - _id: Types.ObjectId; - name: string; - workspace: Types.ObjectId; - environment: string; - user: Types.ObjectId; - serviceAccount: Types.ObjectId; - lastUsed: Date; - expiresAt: Date; - secretHash: string; - encryptedKey: string; - iv: string; - tag: string; - permissions: string[]; + _id: Types.ObjectId; + name: string; + workspace: Types.ObjectId; + environment: string; + user: Types.ObjectId; + serviceAccount: Types.ObjectId; + lastUsed: Date; + expiresAt: Date; + secretHash: string; + encryptedKey: string; + iv: string; + tag: string; + secretPath: string; + permissions: string[]; } const serviceTokenDataSchema = new Schema( - { - name: { - type: String, - required: true - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - environment: { - type: String, - required: true - }, - user: { - type: Schema.Types.ObjectId, - ref: 'User', - required: true - }, - serviceAccount: { - type: Schema.Types.ObjectId, - ref: 'ServiceAccount' - }, - lastUsed: { - type: Date - }, - expiresAt: { - type: Date - }, - secretHash: { - type: String, - required: true, - select: false - }, - encryptedKey: { - type: String, - select: false - }, - iv: { - type: String, - select: false - }, - tag: { - type: String, - select: false - }, - permissions: { - type: [String], - enum: ['read', 'write'], - default: ['read'] - } - }, - { - timestamps: true - } + { + name: { + type: String, + required: true, + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + environment: { + type: String, + required: true, + }, + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: "ServiceAccount", + }, + lastUsed: { + type: Date, + }, + expiresAt: { + type: Date, + }, + secretHash: { + type: String, + required: true, + select: false, + }, + encryptedKey: { + type: String, + select: false, + }, + iv: { + type: String, + select: false, + }, + tag: { + type: String, + select: false, + }, + permissions: { + type: [String], + enum: ["read", "write"], + default: ["read"], + }, + secretPath: { + type: String, + default: "/", + required: true, + }, + }, + { + timestamps: true, + } +); + +const ServiceTokenData = model( + "ServiceTokenData", + serviceTokenDataSchema ); -const ServiceTokenData = model('ServiceTokenData', serviceTokenDataSchema); - export default ServiceTokenData; diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 075d0d392..e96064715 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -1,72 +1,79 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireAuth, - requireWorkspaceAuth, - requireServiceTokenDataAuth, - validateRequest -} from '../../middleware'; -import { param, body } from 'express-validator'; + requireAuth, + requireWorkspaceAuth, + requireServiceTokenDataAuth, + validateRequest, +} from "../../middleware"; +import { param, body } from "express-validator"; import { - ADMIN, - MEMBER, - PERMISSION_WRITE_SECRETS, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN -} from '../../variables'; -import { serviceTokenDataController } from '../../controllers/v2'; + ADMIN, + MEMBER, + PERMISSION_WRITE_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, +} from "../../variables"; +import { serviceTokenDataController } from "../../controllers/v2"; router.get( - '/', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN] - }), - serviceTokenDataController.getServiceTokenData + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN], + }), + serviceTokenDataController.getServiceTokenData ); router.post( - '/', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', - locationEnvironment: 'body', - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }), - body('name').exists().isString().trim(), - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('encryptedKey').exists().isString().trim(), - body('iv').exists().isString().trim(), - body('tag').exists().isString().trim(), - body('expiresIn').exists().isNumeric(), // measured in ms - body('permissions').isArray({ min: 1 }).custom((value: string[]) => { - const allowedPermissions = ['read', 'write']; - const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); - if (invalidValues.length > 0) { - throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`); - } + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + }), + body("name").exists().isString().trim(), + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("encryptedKey").exists().isString().trim(), + body("iv").exists().isString().trim(), + body("secretPath").isString().default("/").trim(), + body("tag").exists().isString().trim(), + body("expiresIn").exists().isNumeric(), // measured in ms + body("permissions") + .isArray({ min: 1 }) + .custom((value: string[]) => { + const allowedPermissions = ["read", "write"]; + const invalidValues = value.filter( + (v) => !allowedPermissions.includes(v) + ); + if (invalidValues.length > 0) { + throw new Error( + `permissions contains invalid values: ${invalidValues.join(", ")}` + ); + } - return true + return true; }), - validateRequest, - serviceTokenDataController.createServiceTokenData + validateRequest, + serviceTokenDataController.createServiceTokenData ); router.delete( - '/:serviceTokenDataId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireServiceTokenDataAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('serviceTokenDataId').exists().trim(), - validateRequest, - serviceTokenDataController.deleteServiceTokenData + "/:serviceTokenDataId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireServiceTokenDataAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("serviceTokenDataId").exists().trim(), + validateRequest, + serviceTokenDataController.deleteServiceTokenData ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 6a18fdf73..6a3d7af88 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,157 +1,162 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireAuth, - requireWorkspaceAuth, - validateRequest -} from '../../middleware'; -import { body, param, query } from 'express-validator'; -import { secretsController } from '../../controllers/v3'; + requireAuth, + requireWorkspaceAuth, + validateRequest, +} from "../../middleware"; +import { body, param, query } from "express-validator"; +import { secretsController } from "../../controllers/v3"; import { - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT, - ADMIN, - MEMBER, - PERMISSION_WRITE_SECRETS, - SECRET_SHARED, - SECRET_PERSONAL, - PERMISSION_READ_SECRETS -} from '../../variables'; + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ADMIN, + MEMBER, + PERMISSION_WRITE_SECRETS, + SECRET_SHARED, + SECRET_PERSONAL, + PERMISSION_READ_SECRETS, +} from "../../variables"; router.get( - '/', - query('workspaceId').exists().isString().trim(), - query('environment').exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT - ] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'query', - locationEnvironment: 'query', - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - }), - secretsController.getSecrets + "/", + query("workspaceId").exists().isString().trim(), + query("environment").exists().isString().trim(), + query("secretPath").default("/").isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "query", + locationEnvironment: "query", + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + }), + secretsController.getSecrets ); router.post( - '/:secretName', - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body('secretKeyCiphertext').exists().isString().trim(), - body('secretKeyIV').exists().isString().trim(), - body('secretKeyTag').exists().isString().trim(), - body('secretValueCiphertext').exists().isString().trim(), - body('secretValueIV').exists().isString().trim(), - body('secretValueTag').exists().isString().trim(), - body('secretCommentCiphertext').optional().isString().trim(), - body('secretCommentIV').optional().isString().trim(), - body('secretCommentTag').optional().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT - ] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', - locationEnvironment: 'body', - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - }), - secretsController.createSecret + "/:secretName", + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + body("secretKeyCiphertext").exists().isString().trim(), + body("secretKeyIV").exists().isString().trim(), + body("secretKeyTag").exists().isString().trim(), + body("secretValueCiphertext").exists().isString().trim(), + body("secretValueIV").exists().isString().trim(), + body("secretValueTag").exists().isString().trim(), + body("secretCommentCiphertext").optional().isString().trim(), + body("secretCommentIV").optional().isString().trim(), + body("secretCommentTag").optional().isString().trim(), + body("secretPath").default("/").isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + }), + secretsController.createSecret ); router.get( - '/:secretName', - param('secretName').exists().isString().trim(), - query('workspaceId').exists().isString().trim(), - query('environment').exists().isString().trim(), - query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, - requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT - ] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'query', - locationEnvironment: 'query', - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - }), - secretsController.getSecretByName + "/:secretName", + param("secretName").exists().isString().trim(), + query("workspaceId").exists().isString().trim(), + query("environment").exists().isString().trim(), + query("secretPath").default("/").isString().trim(), + query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "query", + locationEnvironment: "query", + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + }), + secretsController.getSecretByName ); router.patch( - '/:secretName', - param('secretName').exists().isString().trim(), - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body('secretValueCiphertext').exists().isString().trim(), - body('secretValueIV').exists().isString().trim(), - body('secretValueTag').exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT - ] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', - locationEnvironment: 'body', - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - }), - secretsController.updateSecretByName + "/:secretName", + param("secretName").exists().isString().trim(), + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + body("secretValueCiphertext").exists().isString().trim(), + body("secretValueIV").exists().isString().trim(), + body("secretValueTag").exists().isString().trim(), + body("secretPath").default("/").isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + }), + secretsController.updateSecretByName ); router.delete( - '/:secretName', - param('secretName').exists().isString().trim(), - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, - requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT - ] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', - locationEnvironment: 'body', - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - }), - secretsController.deleteSecretByName + "/:secretName", + param("secretName").exists().isString().trim(), + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("secretPath").default("/").isString().trim(), + body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + }), + secretsController.deleteSecretByName ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index aac2bc39a..9344612c1 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -174,6 +174,11 @@ export const searchByFolderIdWithDir = ( // to get folder of a path given // Like /frontend/folder#1 export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { + // corner case when its just / return root + if (searchPath === "/") { + return folders.id === "root" ? folders : undefined; + } + const path = searchPath.split("/").filter(Boolean); const queue = [folders]; let segment: TFolderSchema | undefined; diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts index 5b5b73e5f..d7c1c9644 100644 --- a/backend/src/services/SecretService.ts +++ b/backend/src/services/SecretService.ts @@ -1,171 +1,169 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; +import { ISecret } from "../models"; import { - ISecret -} from '../models'; + CreateSecretParams, + GetSecretsParams, + GetSecretParams, + UpdateSecretParams, + DeleteSecretParams, +} from "../interfaces/services/SecretService"; import { - CreateSecretParams, - GetSecretsParams, - GetSecretParams, - UpdateSecretParams, - DeleteSecretParams -} from '../interfaces/services/SecretService'; -import { - createSecretBlindIndexDataHelper, - getSecretBlindIndexSaltHelper, - generateSecretBlindIndexWithSaltHelper, - generateSecretBlindIndexHelper, - createSecretHelper, - getSecretsHelper, - getSecretHelper, - updateSecretHelper, - deleteSecretHelper -} from '../helpers/secrets'; + createSecretBlindIndexDataHelper, + getSecretBlindIndexSaltHelper, + generateSecretBlindIndexWithSaltHelper, + generateSecretBlindIndexHelper, + createSecretHelper, + getSecretsHelper, + getSecretHelper, + updateSecretHelper, + deleteSecretHelper, +} from "../helpers/secrets"; class SecretService { + /** + * Create secret blind index data containing encrypted blind index salt + * for workspace with id [workspaceId] + * @param {Object} obj + * @param {Buffer} obj.salt - 16-byte random salt + * @param {Types.ObjectId} obj.workspaceId + */ + static async createSecretBlindIndexData({ + workspaceId, + }: { + workspaceId: Types.ObjectId; + }) { + return await createSecretBlindIndexDataHelper({ + workspaceId, + }); + } - /** - * Create secret blind index data containing encrypted blind index salt - * for workspace with id [workspaceId] - * @param {Object} obj - * @param {Buffer} obj.salt - 16-byte random salt - * @param {Types.ObjectId} obj.workspaceId - */ - static async createSecretBlindIndexData({ - workspaceId, - }: { - workspaceId: Types.ObjectId; - }) { - return await createSecretBlindIndexDataHelper({ - workspaceId - }); - } + /** + * Get secret blind index salt for workspace with id [workspaceId] + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for + * @returns + */ + static async getSecretBlindIndexSalt({ + workspaceId, + }: { + workspaceId: Types.ObjectId; + }) { + return await getSecretBlindIndexSaltHelper({ + workspaceId, + }); + } - /** - * Get secret blind index salt for workspace with id [workspaceId] - * @param {Object} obj - * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for - * @returns - */ - static async getSecretBlindIndexSalt({ - workspaceId - }: { - workspaceId: Types.ObjectId; - }) { - return await getSecretBlindIndexSaltHelper({ - workspaceId - }); - } + /** + * Generate blind index for secret with name [secretName] + * and salt [salt] + * @param {Object} obj + * @param {Object} obj.secretName - name of secret to generate blind index for + * @param {String} obj.salt - base64-salt + */ + static async generateSecretBlindIndexWithSalt({ + secretName, + salt, + }: { + secretName: string; + salt: string; + }) { + return await generateSecretBlindIndexWithSaltHelper({ + secretName, + salt, + }); + } - /** - * Generate blind index for secret with name [secretName] - * and salt [salt] - * @param {Object} obj - * @param {Object} obj.secretName - name of secret to generate blind index for - * @param {String} obj.salt - base64-salt - */ - static async generateSecretBlindIndexWithSalt({ - secretName, - salt - }: { - secretName: string; - salt: string; - }) { - return await generateSecretBlindIndexWithSaltHelper({ - secretName, - salt - }); - } + /** + * Create and return blind index for secret with + * name [secretName] part of workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to generate blind index for + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + */ + static async generateSecretBlindIndex({ + secretName, + workspaceId, + }: { + secretName: string; + workspaceId: Types.ObjectId; + }) { + return await generateSecretBlindIndexHelper({ + secretName, + workspaceId, + }); + } - /** - * Create and return blind index for secret with - * name [secretName] part of workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.secretName - name of secret to generate blind index for - * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to - */ - static async generateSecretBlindIndex({ - secretName, - workspaceId, - }: { - secretName: string; - workspaceId: Types.ObjectId; - }) { - return await generateSecretBlindIndexHelper({ - secretName, - workspaceId - }); - } - - /** - * Create secret with name [secretName] - * @param {Object} obj - * @param {String} obj.secretName - name of secret to create - * @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for - * @param {String} obj.environment - environment in workspace to create secret for - * @param {'shared' | 'personal'} obj.type - type of secret - * @param {AuthData} obj.authData - authentication data on request - * @returns - */ - static async createSecret(createSecretParams: CreateSecretParams) { - return await createSecretHelper(createSecretParams); - } + /** + * Create secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to create + * @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for + * @param {String} obj.environment - environment in workspace to create secret for + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async createSecret(createSecretParams: CreateSecretParams) { + return await createSecretHelper(createSecretParams); + } - /** - * Get secrets for workspace with id [workspaceId] and environment [environment] - * @param {Object} obj - * @param {Types.ObjectId} obj.workspaceId - id of workspace - * @param {String} obj.environment - environment in workspace - * @param {AuthData} obj.authData - authentication data on request - * @returns - */ - static async getSecrets(getSecretsParams: GetSecretsParams) { - return await getSecretsHelper(getSecretsParams); - } - - /** - * Get secret with name [secretName] - * @param {Object} obj - * @param {String} obj.secretName - name of secret to get - * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to - * @param {String} obj.environment - environment in workspace that secret belongs to - * @param {'shared' | 'personal'} obj.type - type of secret - * @param {AuthData} obj.authData - authentication data on request - * @returns - */ - static async getSecret(getSecretParams: GetSecretParams) { - return await getSecretHelper(getSecretParams); - } - - /** - * Update secret with name [secretName] - * @param {Object} obj - * @param {String} obj.secretName - name of secret to update - * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to - * @param {String} obj.environment - environment in workspace that secret belongs to - * @param {'shared' | 'personal'} obj.type - type of secret - * @param {String} obj.secretValueCiphertext - ciphertext of secret value - * @param {String} obj.secretValueIV - IV of secret value - * @param {String} obj.secretValueTag - tag of secret value - * @param {AuthData} obj.authData - authentication data on request - * @returns - */ - static async updateSecret(updateSecretParams: UpdateSecretParams) { - return await updateSecretHelper(updateSecretParams); - } + /** + * Get secrets for workspace with id [workspaceId] and environment [environment] + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace + * @param {String} obj.environment - environment in workspace + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async getSecrets(getSecretsParams: GetSecretsParams) { + return await getSecretsHelper(getSecretsParams); + } - /** - * Delete secret with name [secretName] - * @param {Object} obj - * @param {String} obj.secretName - name of secret to delete - * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to - * @param {String} obj.environment - environment in workspace that secret belongs to - * @param {'shared' | 'personal'} obj.type - type of secret - * @param {AuthData} obj.authData - authentication data on request - * @returns - */ - static async deleteSecret(deleteSecretParams: DeleteSecretParams) { - return await deleteSecretHelper(deleteSecretParams); - } + /** + * Get secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to get + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async getSecret(getSecretParams: GetSecretParams) { + // TODO(akhilmhdh) The one above is diff. Change this to some other name + return await getSecretHelper(getSecretParams); + } + + /** + * Update secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to update + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {String} obj.secretValueCiphertext - ciphertext of secret value + * @param {String} obj.secretValueIV - IV of secret value + * @param {String} obj.secretValueTag - tag of secret value + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async updateSecret(updateSecretParams: UpdateSecretParams) { + return await updateSecretHelper(updateSecretParams); + } + + /** + * Delete secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to delete + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async deleteSecret(deleteSecretParams: DeleteSecretParams) { + return await deleteSecretHelper(deleteSecretParams); + } } -export default SecretService; \ No newline at end of file +export default SecretService; diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 5886ff776..561bff771 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import crypto from "crypto"; import { Types } from "mongoose"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; @@ -11,6 +12,7 @@ import { Bot, BackupPrivateKey, IntegrationAuth, + ServiceTokenData, } from "../../models"; import { generateKeyPair } from "../../utils/crypto"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; @@ -64,7 +66,7 @@ export const backfillSecretVersions = async () => { ), }); } - console.log("Migration: Secret version migration v1 complete") + console.log("Migration: Secret version migration v1 complete"); }; /** @@ -380,13 +382,15 @@ export const backfillSecretFolders = async () => { }); const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => { - const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv] ? groupSnapByEnv[snapEnv].map(secretVersion => secretVersion._id) : [] + const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv] + ? groupSnapByEnv[snapEnv].map((secretVersion) => secretVersion._id) + : []; return { ...secSnapshot.toObject({ virtuals: false }), _id: new Types.ObjectId(), environment: snapEnv, secretVersions: secretIdsOfEnvGroup, - } + }; }); await SecretSnapshot.insertMany(newSnapshots); @@ -402,5 +406,22 @@ export const backfillSecretFolders = async () => { .limit(50); } - console.log("Migration: Folder migration v1 complete") + console.log("Migration: Folder migration v1 complete"); +}; + +export const backfillServiceToken = async () => { + // Back fill because tags were missing in secret versions + await ServiceTokenData.updateMany( + { + secretPath: { + $exists: false, + }, + }, + { + $set: { + secretPath: "/", + }, + } + ); + console.log("Migration: Service token migration v1 complete"); }; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 61f17bebb..bec142e9e 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -1,30 +1,31 @@ -import * as Sentry from '@sentry/node'; -import { DatabaseService, TelemetryService } from '../../services'; -import { setTransporter } from '../../helpers/nodemailer'; -import { EELicenseService } from '../../ee/services'; -import { initSmtp } from '../../services/smtp'; -import { createTestUserForDevelopment } from '../addDevelopmentUser'; +import * as Sentry from "@sentry/node"; +import { DatabaseService, TelemetryService } from "../../services"; +import { setTransporter } from "../../helpers/nodemailer"; +import { EELicenseService } from "../../ee/services"; +import { initSmtp } from "../../services/smtp"; +import { createTestUserForDevelopment } from "../addDevelopmentUser"; // eslint-disable-next-line @typescript-eslint/no-var-requires -import { validateEncryptionKeysConfig } from './validateConfig'; +import { validateEncryptionKeysConfig } from "./validateConfig"; import { backfillSecretVersions, backfillBots, backfillSecretBlindIndexData, backfillEncryptionMetadata, backfillSecretFolders, -} from './backfillData'; + backfillServiceToken, +} from "./backfillData"; import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts, -} from './reencryptData'; +} from "./reencryptData"; import { getNodeEnv, getMongoURL, getSentryDSN, getClientSecretGoogle, getClientIdGoogle, -} from '../../config'; -import { initializePassport } from '../auth'; +} from "../../config"; +import { initializePassport } from "../auth"; /** * Prepare Infisical upon startup. This includes tasks like: @@ -75,6 +76,7 @@ export const setup = async () => { await backfillSecretBlindIndexData(); await backfillEncryptionMetadata(); await backfillSecretFolders(); + await backfillServiceToken(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY @@ -85,7 +87,7 @@ export const setup = async () => { Sentry.init({ dsn: await getSentryDSN(), tracesSampleRate: 1.0, - debug: (await getNodeEnv()) === 'production' ? false : true, + debug: (await getNodeEnv()) === "production" ? false : true, environment: await getNodeEnv(), });