feat(folder-scoped-st): implemented backend api for folder scoped service tokens

This commit is contained in:
akhilmhdh
2023-06-09 23:44:33 +05:30
parent 00fae0023a
commit 6cb0a20675
11 changed files with 892 additions and 719 deletions
@@ -1,29 +1,25 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import crypto from 'crypto'; import crypto from "crypto";
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
import { User, ServiceAccount, ServiceTokenData } from "../../models";
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
import { import {
User, PERMISSION_READ_SECRETS,
ServiceAccount, AUTH_MODE_JWT,
ServiceTokenData AUTH_MODE_SERVICE_ACCOUNT,
} from '../../models'; AUTH_MODE_SERVICE_TOKEN,
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; } from "../../variables";
import { import { getSaltRounds } from "../../config";
PERMISSION_READ_SECRETS, import { BadRequestError } from "../../utils/errors";
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 * Return service token data associated with service token on request
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getServiceTokenData = async (req: Request, res: Response) => { export const getServiceTokenData = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return Infisical Token data' #swagger.summary = 'Return Infisical Token data'
#swagger.description = '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({ if (!(req.authData.authPayload instanceof ServiceTokenData))
message: 'Failed accepted client validation for service token data' throw BadRequestError({
message: "Failed accepted client validation for service token data",
}); });
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData.findById(
.findById(req.authData.authPayload._id) req.authData.authPayload._id
.select('+encryptedKey +iv +tag') )
.populate('user'); .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 * Create new service token data for workspace with id [workspaceId] and
* environment [environment]. * environment [environment].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; let serviceTokenData;
const { const {
name, name,
workspaceId, workspaceId,
environment, environment,
encryptedKey, encryptedKey,
iv, iv,
tag, tag,
expiresIn, expiresIn,
permissions secretPath,
} = req.body; permissions,
} = req.body;
const secret = crypto.randomBytes(16).toString('hex'); const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt; let expiresAt;
if (expiresIn) { if (expiresIn) {
expiresAt = new Date() expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
} }
let user, serviceAccount; let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) { if (
serviceAccount = req.authData.authPayload._id; req.authData.authMode === AUTH_MODE_JWT &&
} req.authData.authPayload instanceof User
) {
serviceTokenData = await new ServiceTokenData({ user = req.authData.authPayload._id;
name, }
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data if (
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); 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({ if (!serviceTokenData) throw new Error("Failed to find service token data");
serviceToken,
serviceTokenData const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
});
} return res.status(200).send({
serviceToken,
serviceTokenData,
});
};
/** /**
* Delete service token data with id [serviceTokenDataId]. * Delete service token data with id [serviceTokenDataId].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteServiceTokenData = async (req: Request, res: Response) => { 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({ return res.status(200).send({
serviceTokenData serviceTokenData,
}); });
} };
+148 -147
View File
@@ -1,183 +1,184 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { SecretService, EventService } from "../../services";
SecretService, import { eventPushSecrets } from "../../events";
TelemetryService,
EventService
} from '../../services';
import { eventPushSecrets } from '../../events';
import { getAuthDataPayloadIdObj } from '../../utils/auth';
import { BadRequestError } from '../../utils/errors';
/** /**
* Get secrets for workspace with id [workspaceId] and environment * Get secrets for workspace with id [workspaceId] and environment
* [environment] * [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
authData: req.authData secretPath,
}); authData: req.authData,
});
return res.status(200).send({ return res.status(200).send({
secrets secrets,
}); });
} };
/** /**
* Get secret with name [secretName] * Get secret with name [secretName]
* @param req * @param req
* @param res * @param res
*/ */
export const getSecretByName = async (req: Request, res: Response) => { export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const type = req.query.type as 'shared' | 'personal' | undefined; const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData secretPath,
}); authData: req.authData,
});
return res.status(200).send({
secret return res.status(200).send({
}); secret,
} });
};
/** /**
* Create secret with name [secretName] * Create secret with name [secretName]
* @param req * @param req
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
environment, environment,
type, type,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag,
} = req.body; secretPath = "/",
} = 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
} : {})
});
await EventService.handleEvent({ const secret = await SecretService.createSecret({
event: eventPushSecrets({ secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) type,
}); authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
...(secretCommentCiphertext && secretCommentIV && secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
}
: {}),
});
const secretWithoutBlindIndex = secret.toObject(); await EventService.handleEvent({
delete secretWithoutBlindIndex.secretBlindIndex; event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
return res.status(200).send({ environment,
secret: secretWithoutBlindIndex }),
}); });
}
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex,
});
};
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
* @param req * @param req
* @param res * @param res
*/ */
export const updateSecretByName = async (req: Request, res: Response) => { export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
environment, environment,
type, type,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
} = req.body; secretPath = "/",
} = req.body;
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
}); secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({ await EventService.handleEvent({
secret event: eventPushSecrets({
}); workspaceId: new Types.ObjectId(workspaceId),
} environment,
}),
});
return res.status(200).send({
secret,
});
};
/** /**
* Delete secret with name [secretName] * Delete secret with name [secretName]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretPath = "/" } = req.body;
workspaceId,
environment,
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData
});
await EventService.handleEvent({ const { secret } = await SecretService.deleteSecret({
event: eventPushSecrets({ secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId,
environment environment,
}) type,
}); authData: req.authData,
secretPath,
});
return res.status(200).send({ await EventService.handleEvent({
secret event: eventPushSecrets({
}); workspaceId: new Types.ObjectId(workspaceId),
} environment,
}),
});
return res.status(200).send({
secret,
});
};
+153 -39
View File
@@ -1,19 +1,24 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
CreateSecretParams, CreateSecretParams,
GetSecretsParams, GetSecretsParams,
GetSecretParams, GetSecretParams,
UpdateSecretParams, UpdateSecretParams,
DeleteSecretParams, DeleteSecretParams,
} from '../interfaces/services/SecretService'; } from "../interfaces/services/SecretService";
import { Secret, ISecret, SecretBlindIndexData } from '../models'; import {
import { SecretVersion } from '../ee/models'; Secret,
ISecret,
SecretBlindIndexData,
ServiceTokenData,
} from "../models";
import { SecretVersion } from "../ee/models";
import { import {
BadRequestError, BadRequestError,
SecretNotFoundError, SecretNotFoundError,
SecretBlindIndexDataNotFoundError, SecretBlindIndexDataNotFoundError,
InternalServerError, InternalServerError,
} from '../utils/errors'; } from "../utils/errors";
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
@@ -24,20 +29,44 @@ import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
} from '../variables'; } from "../variables";
import crypto from 'crypto'; import crypto from "crypto";
import * as argon2 from 'argon2'; import * as argon2 from "argon2";
import { import {
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
} from '../utils/crypto'; } from "../utils/crypto";
import { getEncryptionKey, client, getRootEncryptionKey } from '../config'; import { getEncryptionKey, client, getRootEncryptionKey } from "../config";
import { TelemetryService } from '../services'; import { TelemetryService } from "../services";
import { EESecretService, EELogService } from '../ee/services'; import { EESecretService, EELogService } from "../ee/services";
import { import {
getAuthDataPayloadIdObj, getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj, 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] * Create secret blind index data containing encrypted blind index [salt]
@@ -46,12 +75,12 @@ import {
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
export const createSecretBlindIndexDataHelper = async ({ export const createSecretBlindIndexDataHelper = async ({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
// initialize random blind index salt for workspace // 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 encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
@@ -99,7 +128,7 @@ export const createSecretBlindIndexDataHelper = async ({
* @returns * @returns
*/ */
export const getSecretBlindIndexSaltHelper = async ({ export const getSecretBlindIndexSaltHelper = async ({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -108,7 +137,7 @@ export const getSecretBlindIndexSaltHelper = async ({
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId,
}).select('+algorithm +keyEncoding'); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
@@ -136,7 +165,7 @@ export const getSecretBlindIndexSaltHelper = async ({
} }
throw InternalServerError({ 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 * @param {String} obj.salt - base64-salt
*/ */
export const generateSecretBlindIndexWithSaltHelper = async ({ export const generateSecretBlindIndexWithSaltHelper = async ({
secretName, secretName,
salt salt,
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
@@ -158,14 +187,14 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
const secretBlindIndex = ( const secretBlindIndex = (
await argon2.hash(secretName, { await argon2.hash(secretName, {
type: argon2.argon2id, type: argon2.argon2id,
salt: Buffer.from(salt, 'base64'), salt: Buffer.from(salt, "base64"),
saltLength: 16, // default 16 bytes saltLength: 16, // default 16 bytes
memoryCost: 65536, // default pool of 64 MiB per thread. memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32, hashLength: 32,
parallelism: 1, parallelism: 1,
raw: true, raw: true,
}) })
).toString('base64'); ).toString("base64");
return secretBlindIndex; return secretBlindIndex;
}; };
@@ -178,8 +207,8 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/ */
export const generateSecretBlindIndexHelper = async ({ export const generateSecretBlindIndexHelper = async ({
secretName, secretName,
workspaceId workspaceId,
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
@@ -190,7 +219,7 @@ export const generateSecretBlindIndexHelper = async ({
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId,
}).select('+algorithm +keyEncoding'); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
@@ -231,9 +260,9 @@ export const generateSecretBlindIndexHelper = async ({
return secretBlindIndex; return secretBlindIndex;
} }
throw InternalServerError({ 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, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
folderId, secretPath = "/",
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), 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({ const exists = await Secret.exists({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
folder: folderId,
type, type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
}); });
if (exists) if (exists)
throw BadRequestError({ throw BadRequestError({
message: 'Failed to create secret that already exists', message: "Failed to create secret that already exists",
}); });
if (type === SECRET_PERSONAL) { if (type === SECRET_PERSONAL) {
@@ -287,6 +330,7 @@ export const createSecretHelper = async ({
const exists = await Secret.exists({ const exists = await Secret.exists({
secretBlindIndex, secretBlindIndex,
folder: folderId,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED, type: SECRET_SHARED,
}); });
@@ -294,7 +338,7 @@ export const createSecretHelper = async ({
if (!exists) if (!exists)
throw BadRequestError({ throw BadRequestError({
message: 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, version: secret.version,
workspace: secret.workspace, workspace: secret.workspace,
type, type,
folder: folderId,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
isDeleted: false, isDeleted: false,
@@ -372,7 +417,7 @@ export const createSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets added', event: "secrets added",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -380,6 +425,7 @@ export const createSecretHelper = async ({
numberOfSecrets: 1, numberOfSecrets: 1,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -398,16 +444,30 @@ export const createSecretHelper = async ({
* @returns * @returns
*/ */
export const getSecretsHelper = async ({ export const getSecretsHelper = async ({
workspaceId, workspaceId,
environment, environment,
authData authData,
secretPath = "/",
}: GetSecretsParams) => { }: GetSecretsParams) => {
let secrets: ISecret[] = []; 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 // get personal secrets first
secrets = await Secret.find({ secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: SECRET_PERSONAL, type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData),
}); });
@@ -417,6 +477,7 @@ export const getSecretsHelper = async ({
await Secret.find({ await Secret.find({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED,
secretBlindIndex: { secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex), $nin: secrets.map((secret) => secret.secretBlindIndex),
@@ -445,7 +506,7 @@ export const getSecretsHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pulled', event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -453,6 +514,7 @@ export const getSecretsHelper = async ({
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -478,18 +540,32 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/",
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
}); });
let secret: ISecret | null = null; 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) // try getting personal secret first (if exists)
secret = await Secret.findOne({ secret = await Secret.findOne({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: type ?? SECRET_PERSONAL, type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
}); });
@@ -501,6 +577,7 @@ export const getSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED,
}); });
} }
@@ -528,7 +605,7 @@ export const getSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pull', event: "secrets pull",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -536,6 +613,7 @@ export const getSecretHelper = async ({
numberOfSecrets: 1, numberOfSecrets: 1,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -568,6 +646,7 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath,
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
@@ -575,6 +654,18 @@ export const updateSecretHelper = async ({
}); });
let secret: ISecret | null = null; 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) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
@@ -583,6 +674,7 @@ export const updateSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type, type,
}, },
{ {
@@ -604,6 +696,7 @@ export const updateSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
folder: folderId,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData),
}, },
{ {
@@ -624,6 +717,7 @@ export const updateSecretHelper = async ({
secret: secret._id, secret: secret._id,
version: secret.version, version: secret.version,
workspace: secret.workspace, workspace: secret.workspace,
folder: folderId,
type, type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
@@ -672,7 +766,7 @@ export const updateSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets modified', event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -680,6 +774,7 @@ export const updateSecretHelper = async ({
numberOfSecrets: 1, numberOfSecrets: 1,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -705,12 +800,26 @@ export const deleteSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/",
}: DeleteSecretParams) => { }: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), 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 secrets: ISecret[] = [];
let secret: ISecret | null = null; let secret: ISecret | null = null;
@@ -719,6 +828,7 @@ export const deleteSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
}); });
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
@@ -726,16 +836,19 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
folder: folderId,
}); });
await Secret.deleteMany({ await Secret.deleteMany({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
}); });
} else { } else {
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
secretBlindIndex, secretBlindIndex,
folder: folderId,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
@@ -782,7 +895,7 @@ export const deleteSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets deleted', event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -790,6 +903,7 @@ export const deleteSecretHelper = async ({
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -800,4 +914,4 @@ export const deleteSecretHelper = async ({
secrets, secrets,
secret, secret,
}; };
}; };
@@ -5,7 +5,6 @@ export interface CreateSecretParams {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
folderId?: string;
type: "shared" | "personal"; type: "shared" | "personal";
authData: AuthData; authData: AuthData;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -17,17 +16,20 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretPath: string;
} }
export interface GetSecretsParams { export interface GetSecretsParams {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
secretPath: string;
authData: AuthData; authData: AuthData;
} }
export interface GetSecretParams { export interface GetSecretParams {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretPath: string;
environment: string; environment: string;
type?: "shared" | "personal"; type?: "shared" | "personal";
authData: AuthData; authData: AuthData;
@@ -42,7 +44,7 @@ export interface UpdateSecretParams {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
folderId?: string; secretPath: string;
} }
export interface DeleteSecretParams { export interface DeleteSecretParams {
@@ -51,4 +53,5 @@ export interface DeleteSecretParams {
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
authData: AuthData; authData: AuthData;
secretPath: string;
} }
+80 -71
View File
@@ -1,79 +1,88 @@
import { Schema, model, Types, Document } from 'mongoose'; import { Schema, model, Types, Document } from "mongoose";
export interface IServiceTokenData extends Document { export interface IServiceTokenData extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; environment: string;
user: Types.ObjectId; user: Types.ObjectId;
serviceAccount: Types.ObjectId; serviceAccount: Types.ObjectId;
lastUsed: Date; lastUsed: Date;
expiresAt: Date; expiresAt: Date;
secretHash: string; secretHash: string;
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
permissions: string[]; secretPath: string;
permissions: string[];
} }
const serviceTokenDataSchema = new Schema<IServiceTokenData>( const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{ {
name: { name: {
type: String, type: String,
required: true required: true,
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
}, },
environment: { environment: {
type: String, type: String,
required: true required: true,
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: "User",
required: true required: true,
}, },
serviceAccount: { serviceAccount: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'ServiceAccount' ref: "ServiceAccount",
}, },
lastUsed: { lastUsed: {
type: Date type: Date,
}, },
expiresAt: { expiresAt: {
type: Date type: Date,
}, },
secretHash: { secretHash: {
type: String, type: String,
required: true, required: true,
select: false select: false,
}, },
encryptedKey: { encryptedKey: {
type: String, type: String,
select: false select: false,
}, },
iv: { iv: {
type: String, type: String,
select: false select: false,
}, },
tag: { tag: {
type: String, type: String,
select: false select: false,
}, },
permissions: { permissions: {
type: [String], type: [String],
enum: ['read', 'write'], enum: ["read", "write"],
default: ['read'] default: ["read"],
} },
}, secretPath: {
{ type: String,
timestamps: true default: "/",
} required: true,
},
},
{
timestamps: true,
}
);
const ServiceTokenData = model<IServiceTokenData>(
"ServiceTokenData",
serviceTokenDataSchema
); );
const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema);
export default ServiceTokenData; export default ServiceTokenData;
+64 -57
View File
@@ -1,72 +1,79 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenDataAuth, requireServiceTokenDataAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { param, body } from 'express-validator'; import { param, body } from "express-validator";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN AUTH_MODE_SERVICE_TOKEN,
} from '../../variables'; } from "../../variables";
import { serviceTokenDataController } from '../../controllers/v2'; import { serviceTokenDataController } from "../../controllers/v2";
router.get( router.get(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN] acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN],
}), }),
serviceTokenDataController.getServiceTokenData serviceTokenDataController.getServiceTokenData
); );
router.post( router.post(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS] requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), }),
body('name').exists().isString().trim(), body("name").exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('encryptedKey').exists().isString().trim(), body("encryptedKey").exists().isString().trim(),
body('iv').exists().isString().trim(), body("iv").exists().isString().trim(),
body('tag').exists().isString().trim(), body("secretPath").isString().default("/").trim(),
body('expiresIn').exists().isNumeric(), // measured in ms body("tag").exists().isString().trim(),
body('permissions').isArray({ min: 1 }).custom((value: string[]) => { body("expiresIn").exists().isNumeric(), // measured in ms
const allowedPermissions = ['read', 'write']; body("permissions")
const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); .isArray({ min: 1 })
if (invalidValues.length > 0) { .custom((value: string[]) => {
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`); 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, validateRequest,
serviceTokenDataController.createServiceTokenData serviceTokenDataController.createServiceTokenData
); );
router.delete( router.delete(
'/:serviceTokenDataId', "/:serviceTokenDataId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireServiceTokenDataAuth({ requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('serviceTokenDataId').exists().trim(), param("serviceTokenDataId").exists().trim(),
validateRequest, validateRequest,
serviceTokenDataController.deleteServiceTokenData serviceTokenDataController.deleteServiceTokenData
); );
export default router; export default router;
+143 -138
View File
@@ -1,157 +1,162 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { body, param, query } from 'express-validator'; import { body, param, query } from "express-validator";
import { secretsController } from '../../controllers/v3'; import { secretsController } from "../../controllers/v3";
import { import {
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
ADMIN, ADMIN,
MEMBER, MEMBER,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
PERMISSION_READ_SECRETS PERMISSION_READ_SECRETS,
} from '../../variables'; } from "../../variables";
router.get( router.get(
'/', "/",
query('workspaceId').exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query('environment').exists().isString().trim(), query("environment").exists().isString().trim(),
validateRequest, query("secretPath").default("/").isString().trim(),
requireAuth({ validateRequest,
acceptedAuthModes: [ requireAuth({
AUTH_MODE_JWT, acceptedAuthModes: [
AUTH_MODE_API_KEY, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_TOKEN,
] AUTH_MODE_SERVICE_ACCOUNT,
}), ],
requireWorkspaceAuth({ }),
acceptedRoles: [ADMIN, MEMBER], requireWorkspaceAuth({
locationWorkspaceId: 'query', acceptedRoles: [ADMIN, MEMBER],
locationEnvironment: 'query', locationWorkspaceId: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], locationEnvironment: "query",
requireBlindIndicesEnabled: true, requiredPermissions: [PERMISSION_READ_SECRETS],
}), requireBlindIndicesEnabled: true,
secretsController.getSecrets }),
secretsController.getSecrets
); );
router.post( router.post(
'/:secretName', "/:secretName",
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretKeyCiphertext').exists().isString().trim(), body("secretKeyCiphertext").exists().isString().trim(),
body('secretKeyIV').exists().isString().trim(), body("secretKeyIV").exists().isString().trim(),
body('secretKeyTag').exists().isString().trim(), body("secretKeyTag").exists().isString().trim(),
body('secretValueCiphertext').exists().isString().trim(), body("secretValueCiphertext").exists().isString().trim(),
body('secretValueIV').exists().isString().trim(), body("secretValueIV").exists().isString().trim(),
body('secretValueTag').exists().isString().trim(), body("secretValueTag").exists().isString().trim(),
body('secretCommentCiphertext').optional().isString().trim(), body("secretCommentCiphertext").optional().isString().trim(),
body('secretCommentIV').optional().isString().trim(), body("secretCommentIV").optional().isString().trim(),
body('secretCommentTag').optional().isString().trim(), body("secretCommentTag").optional().isString().trim(),
validateRequest, body("secretPath").default("/").isString().trim(),
requireAuth({ validateRequest,
acceptedAuthModes: [ requireAuth({
AUTH_MODE_JWT, acceptedAuthModes: [
AUTH_MODE_API_KEY, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_TOKEN,
] AUTH_MODE_SERVICE_ACCOUNT,
}), ],
requireWorkspaceAuth({ }),
acceptedRoles: [ADMIN, MEMBER], requireWorkspaceAuth({
locationWorkspaceId: 'body', acceptedRoles: [ADMIN, MEMBER],
locationEnvironment: 'body', locationWorkspaceId: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], locationEnvironment: "body",
requireBlindIndicesEnabled: true, requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), requireBlindIndicesEnabled: true,
secretsController.createSecret }),
secretsController.createSecret
); );
router.get( router.get(
'/:secretName', "/:secretName",
param('secretName').exists().isString().trim(), param("secretName").exists().isString().trim(),
query('workspaceId').exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query('environment').exists().isString().trim(), query("environment").exists().isString().trim(),
query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), query("secretPath").default("/").isString().trim(),
validateRequest, query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
requireAuth({ validateRequest,
acceptedAuthModes: [ requireAuth({
AUTH_MODE_JWT, acceptedAuthModes: [
AUTH_MODE_API_KEY, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_TOKEN,
] AUTH_MODE_SERVICE_ACCOUNT,
}), ],
requireWorkspaceAuth({ }),
acceptedRoles: [ADMIN, MEMBER], requireWorkspaceAuth({
locationWorkspaceId: 'query', acceptedRoles: [ADMIN, MEMBER],
locationEnvironment: 'query', locationWorkspaceId: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], locationEnvironment: "query",
requireBlindIndicesEnabled: true, requiredPermissions: [PERMISSION_READ_SECRETS],
}), requireBlindIndicesEnabled: true,
secretsController.getSecretByName }),
secretsController.getSecretByName
); );
router.patch( router.patch(
'/:secretName', "/:secretName",
param('secretName').exists().isString().trim(), param("secretName").exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretValueCiphertext').exists().isString().trim(), body("secretValueCiphertext").exists().isString().trim(),
body('secretValueIV').exists().isString().trim(), body("secretValueIV").exists().isString().trim(),
body('secretValueTag').exists().isString().trim(), body("secretValueTag").exists().isString().trim(),
validateRequest, body("secretPath").default("/").isString().trim(),
requireAuth({ validateRequest,
acceptedAuthModes: [ requireAuth({
AUTH_MODE_JWT, acceptedAuthModes: [
AUTH_MODE_API_KEY, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_TOKEN,
] AUTH_MODE_SERVICE_ACCOUNT,
}), ],
requireWorkspaceAuth({ }),
acceptedRoles: [ADMIN, MEMBER], requireWorkspaceAuth({
locationWorkspaceId: 'body', acceptedRoles: [ADMIN, MEMBER],
locationEnvironment: 'body', locationWorkspaceId: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], locationEnvironment: "body",
requireBlindIndicesEnabled: true, requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), requireBlindIndicesEnabled: true,
secretsController.updateSecretByName }),
secretsController.updateSecretByName
); );
router.delete( router.delete(
'/:secretName', "/:secretName",
param('secretName').exists().isString().trim(), param("secretName").exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body("secretPath").default("/").isString().trim(),
validateRequest, body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
requireAuth({ validateRequest,
acceptedAuthModes: [ requireAuth({
AUTH_MODE_JWT, acceptedAuthModes: [
AUTH_MODE_API_KEY, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_TOKEN,
] AUTH_MODE_SERVICE_ACCOUNT,
}), ],
requireWorkspaceAuth({ }),
acceptedRoles: [ADMIN, MEMBER], requireWorkspaceAuth({
locationWorkspaceId: 'body', acceptedRoles: [ADMIN, MEMBER],
locationEnvironment: 'body', locationWorkspaceId: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], locationEnvironment: "body",
requireBlindIndicesEnabled: true, requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), requireBlindIndicesEnabled: true,
secretsController.deleteSecretByName }),
secretsController.deleteSecretByName
); );
export default router; export default router;
+5
View File
@@ -174,6 +174,11 @@ export const searchByFolderIdWithDir = (
// to get folder of a path given // to get folder of a path given
// Like /frontend/folder#1 // Like /frontend/folder#1
export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { 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 path = searchPath.split("/").filter(Boolean);
const queue = [folders]; const queue = [folders];
let segment: TFolderSchema | undefined; let segment: TFolderSchema | undefined;
+157 -159
View File
@@ -1,171 +1,169 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { ISecret } from "../models";
import { import {
ISecret CreateSecretParams,
} from '../models'; GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams,
} from "../interfaces/services/SecretService";
import { import {
CreateSecretParams, createSecretBlindIndexDataHelper,
GetSecretsParams, getSecretBlindIndexSaltHelper,
GetSecretParams, generateSecretBlindIndexWithSaltHelper,
UpdateSecretParams, generateSecretBlindIndexHelper,
DeleteSecretParams createSecretHelper,
} from '../interfaces/services/SecretService'; getSecretsHelper,
import { getSecretHelper,
createSecretBlindIndexDataHelper, updateSecretHelper,
getSecretBlindIndexSaltHelper, deleteSecretHelper,
generateSecretBlindIndexWithSaltHelper, } from "../helpers/secrets";
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper
} from '../helpers/secrets';
class SecretService { 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 * Get secret blind index salt for workspace with id [workspaceId]
* for workspace with id [workspaceId] * @param {Object} obj
* @param {Object} obj * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @param {Buffer} obj.salt - 16-byte random salt * @returns
* @param {Types.ObjectId} obj.workspaceId */
*/ static async getSecretBlindIndexSalt({
static async createSecretBlindIndexData({ workspaceId,
workspaceId, }: {
}: { workspaceId: Types.ObjectId;
workspaceId: Types.ObjectId; }) {
}) { return await getSecretBlindIndexSaltHelper({
return await createSecretBlindIndexDataHelper({ workspaceId,
workspaceId });
}); }
}
/** /**
* Get secret blind index salt for workspace with id [workspaceId] * Generate blind index for secret with name [secretName]
* @param {Object} obj * and salt [salt]
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for * @param {Object} obj
* @returns * @param {Object} obj.secretName - name of secret to generate blind index for
*/ * @param {String} obj.salt - base64-salt
static async getSecretBlindIndexSalt({ */
workspaceId static async generateSecretBlindIndexWithSalt({
}: { secretName,
workspaceId: Types.ObjectId; salt,
}) { }: {
return await getSecretBlindIndexSaltHelper({ secretName: string;
workspaceId salt: string;
}); }) {
} return await generateSecretBlindIndexWithSaltHelper({
secretName,
salt,
});
}
/** /**
* Generate blind index for secret with name [secretName] * Create and return blind index for secret with
* and salt [salt] * name [secretName] part of workspace with id [workspaceId]
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.secretName - name of secret to generate blind index for * @param {String} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/ */
static async generateSecretBlindIndexWithSalt({ static async generateSecretBlindIndex({
secretName, secretName,
salt workspaceId,
}: { }: {
secretName: string; secretName: string;
salt: string; workspaceId: Types.ObjectId;
}) { }) {
return await generateSecretBlindIndexWithSaltHelper({ return await generateSecretBlindIndexHelper({
secretName, secretName,
salt workspaceId,
}); });
} }
/** /**
* Create and return blind index for secret with * Create secret with name [secretName]
* name [secretName] part of workspace with id [workspaceId] * @param {Object} obj
* @param {Object} obj * @param {String} obj.secretName - name of secret to create
* @param {String} obj.secretName - name of secret to generate blind index for * @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to * @param {String} obj.environment - environment in workspace to create secret for
*/ * @param {'shared' | 'personal'} obj.type - type of secret
static async generateSecretBlindIndex({ * @param {AuthData} obj.authData - authentication data on request
secretName, * @returns
workspaceId, */
}: { static async createSecret(createSecretParams: CreateSecretParams) {
secretName: string; return await createSecretHelper(createSecretParams);
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);
}
/** /**
* Get secrets for workspace with id [workspaceId] and environment [environment] * Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj * @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace * @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment in workspace * @param {String} obj.environment - environment in workspace
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
static async getSecrets(getSecretsParams: GetSecretsParams) { static async getSecrets(getSecretsParams: GetSecretsParams) {
return await getSecretsHelper(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);
}
/** /**
* Delete secret with name [secretName] * Get secret with name [secretName]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.secretName - name of secret to delete * @param {String} obj.secretName - name of secret to get
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in 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 {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
static async deleteSecret(deleteSecretParams: DeleteSecretParams) { static async getSecret(getSecretParams: GetSecretParams) {
return await deleteSecretHelper(deleteSecretParams); // 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; export default SecretService;
+25 -4
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import crypto from "crypto"; import crypto from "crypto";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
@@ -11,6 +12,7 @@ import {
Bot, Bot,
BackupPrivateKey, BackupPrivateKey,
IntegrationAuth, IntegrationAuth,
ServiceTokenData,
} from "../../models"; } from "../../models";
import { generateKeyPair } from "../../utils/crypto"; import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; 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 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 { return {
...secSnapshot.toObject({ virtuals: false }), ...secSnapshot.toObject({ virtuals: false }),
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
environment: snapEnv, environment: snapEnv,
secretVersions: secretIdsOfEnvGroup, secretVersions: secretIdsOfEnvGroup,
} };
}); });
await SecretSnapshot.insertMany(newSnapshots); await SecretSnapshot.insertMany(newSnapshots);
@@ -402,5 +406,22 @@ export const backfillSecretFolders = async () => {
.limit(50); .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");
}; };
+14 -12
View File
@@ -1,30 +1,31 @@
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { DatabaseService, TelemetryService } from '../../services'; import { DatabaseService, TelemetryService } from "../../services";
import { setTransporter } from '../../helpers/nodemailer'; import { setTransporter } from "../../helpers/nodemailer";
import { EELicenseService } from '../../ee/services'; import { EELicenseService } from "../../ee/services";
import { initSmtp } from '../../services/smtp'; import { initSmtp } from "../../services/smtp";
import { createTestUserForDevelopment } from '../addDevelopmentUser'; import { createTestUserForDevelopment } from "../addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
import { validateEncryptionKeysConfig } from './validateConfig'; import { validateEncryptionKeysConfig } from "./validateConfig";
import { import {
backfillSecretVersions, backfillSecretVersions,
backfillBots, backfillBots,
backfillSecretBlindIndexData, backfillSecretBlindIndexData,
backfillEncryptionMetadata, backfillEncryptionMetadata,
backfillSecretFolders, backfillSecretFolders,
} from './backfillData'; backfillServiceToken,
} from "./backfillData";
import { import {
reencryptBotPrivateKeys, reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts, reencryptSecretBlindIndexDataSalts,
} from './reencryptData'; } from "./reencryptData";
import { import {
getNodeEnv, getNodeEnv,
getMongoURL, getMongoURL,
getSentryDSN, getSentryDSN,
getClientSecretGoogle, getClientSecretGoogle,
getClientIdGoogle, getClientIdGoogle,
} from '../../config'; } from "../../config";
import { initializePassport } from '../auth'; import { initializePassport } from "../auth";
/** /**
* Prepare Infisical upon startup. This includes tasks like: * Prepare Infisical upon startup. This includes tasks like:
@@ -75,6 +76,7 @@ export const setup = async () => {
await backfillSecretBlindIndexData(); await backfillSecretBlindIndexData();
await backfillEncryptionMetadata(); await backfillEncryptionMetadata();
await backfillSecretFolders(); await backfillSecretFolders();
await backfillServiceToken();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY
@@ -85,7 +87,7 @@ export const setup = async () => {
Sentry.init({ Sentry.init({
dsn: await getSentryDSN(), dsn: await getSentryDSN(),
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === 'production' ? false : true, debug: (await getNodeEnv()) === "production" ? false : true,
environment: await getNodeEnv(), environment: await getNodeEnv(),
}); });