Merge pull request #633 from akhilmhdh/feat/folders-service-token

Folder scoped service token
This commit is contained in:
BlackMagiq
2023-06-10 11:02:16 +01:00
committed by GitHub
18 changed files with 1064 additions and 788 deletions
+72 -12
View File
@@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ISecret, Secret } from "../../models"; import { ISecret, Secret, ServiceTokenData } from "../../models";
import { IAction, SecretVersion } from "../../ee/models"; import { IAction, SecretVersion } from "../../ee/models";
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
@@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
import { import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken,
searchByFolderId, searchByFolderId,
} from "../../services/FolderService"; } from "../../services/FolderService";
@@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
folderId,
requests, requests,
secretPath,
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId: string;
requests: BatchSecretRequest[]; requests: BatchSecretRequest[];
secretPath: string;
} = req.body; } = req.body;
let folderId = req.body.folderId as string;
const createSecrets: BatchSecret[] = []; const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = [];
@@ -70,6 +72,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) throw BadRequestError({ message: "Folder not found" });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
}
for await (const request of requests) { for await (const request of requests) {
// do a validation // do a validation
@@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: createdSecrets.length, numberOfSecrets: createdSecrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
@@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags, tags: u.tags,
folder: u.folder folder: u.folder,
}) })
); );
@@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: updateSecrets.length, numberOfSecrets: updateSecrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
@@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
folderId, secretPath,
}: { workspaceId: string; environment: string; folderId: string } = req.body; }: {
workspaceId: string;
environment: string;
secretPath?: string;
} = req.body;
let folderId = req.body.folderId;
if (req.user) { if (req.user) {
const hasAccess = await userHasWorkspaceAccess( const hasAccess = await userHasWorkspaceAccess(
@@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret // case: create 1 secret
listOfSecretsToCreate = [req.body.secrets]; listOfSecretsToCreate = [req.body.secrets];
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
}
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
@@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
}); });
@@ -660,6 +707,18 @@ export const getSecrets = async (req: Request, res: Response) => {
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) throw BadRequestError({ message: "Folder not found" });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (folders && secretPath) { if (folders && secretPath) {
if (!folders) throw BadRequestError({ message: "Folder not found" }); if (!folders) throw BadRequestError({ message: "Folder not found" });
const folder = getFolderByPath(folders.nodes, secretPath as string); const folder = getFolderByPath(folders.nodes, secretPath as string);
@@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel, channel,
folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
}); });
@@ -910,13 +970,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags, tags,
...(secretCommentCiphertext !== undefined && ...(secretCommentCiphertext !== undefined &&
secretCommentIV && secretCommentIV &&
secretCommentTag secretCommentTag
? { ? {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
} }
: {}), : {}),
}, },
}, },
@@ -1,20 +1,18 @@
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, import Folder from "../../models/folder";
AUTH_MODE_SERVICE_ACCOUNT, import { getFolderByPath } from "../../services/FolderService";
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
@@ -23,7 +21,7 @@ import { BadRequestError } from '../../utils/errors';
* @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'
@@ -36,30 +34,32 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"serviceTokenData": { "serviceTokenData": {
"type": "object", "type": "object",
$ref: "#/components/schemas/ServiceTokenData", $ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token" "description": "Details of service token"
} }
} }
} }
} }
} }
} }
*/ */
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
@@ -69,65 +69,85 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
* @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 folders = await Folder.findOne({
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); workspace: workspaceId,
environment,
});
let expiresAt; if (folders) {
if (expiresIn) { const folder = getFolderByPath(folders.nodes, secretPath);
expiresAt = new Date() if (folder == undefined) {
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); throw BadRequestError({ message: "Path for service token does not exist" })
} }
}
let user, serviceAccount; const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { let expiresAt;
user = req.authData.authPayload._id; if (expiresIn) {
} expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) { let user, serviceAccount;
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({ if (
name, req.authData.authMode === AUTH_MODE_JWT &&
workspace: workspaceId, req.authData.authPayload instanceof User
environment, ) {
user, user = req.authData.authPayload._id;
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].
@@ -136,11 +156,13 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
* @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,
}); });
} };
+134 -133
View File
@@ -1,13 +1,7 @@
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
@@ -16,19 +10,21 @@ import { BadRequestError } from '../../utils/errors';
* @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]
@@ -36,23 +32,25 @@ export const getSecrets = async (req: Request, res: Response) => {
* @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({ return res.status(200).send({
secret secret,
}); });
} };
/** /**
* Create secret with name [secretName] * Create secret with name [secretName]
@@ -60,55 +58,59 @@ export const getSecretByName = async (req: Request, res: Response) => {
* @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({ const secret = await SecretService.createSecret({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? { secretPath,
secretCommentCiphertext, ...(secretCommentCiphertext && secretCommentIV && secretCommentTag
secretCommentIV, ? {
secretCommentTag secretCommentCiphertext,
} : {}) secretCommentIV,
}); secretCommentTag,
}
: {}),
});
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) }),
}); });
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex; delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({ return res.status(200).send({
secret: secretWithoutBlindIndex secret: secretWithoutBlindIndex,
}); });
} };
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
@@ -116,38 +118,40 @@ export const createSecret = async (req: Request, res: Response) => {
* @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({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) }),
}); });
return res.status(200).send({ return res.status(200).send({
secret secret,
}); });
} };
/** /**
* Delete secret with name [secretName] * Delete secret with name [secretName]
@@ -155,29 +159,26 @@ export const updateSecretByName = async (req: Request, res: Response) => {
* @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({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
type, type,
authData: req.authData authData: req.authData,
}); secretPath,
});
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) }),
}); });
return res.status(200).send({ return res.status(200).send({
secret secret,
}); });
} };
+129 -37
View File
@@ -1,19 +1,25 @@
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'; UnauthorizedRequestError,
} from "../utils/errors";
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
@@ -24,20 +30,21 @@ 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 { getFolderIdFromServiceToken } from "../services/FolderService";
/** /**
* Create secret blind index data containing encrypted blind index [salt] * Create secret blind index data containing encrypted blind index [salt]
@@ -46,12 +53,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 +106,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 +115,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 +143,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 +155,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 +165,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 +185,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 +197,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();
@@ -233,7 +240,7 @@ export const generateSecretBlindIndexHelper = async ({
} }
throw InternalServerError({ throw InternalServerError({
message: 'Failed to generate secret blind index' message: "Failed to generate secret blind index",
}); });
}; };
@@ -262,23 +269,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 UnauthorizedRequestError({ message: "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 +308,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 +316,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 +347,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 +395,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 +403,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 +422,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 UnauthorizedRequestError({ message: "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 +455,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 +484,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 +492,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 +518,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 UnauthorizedRequestError({ message: "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 +555,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 +583,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 +591,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 +624,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 +632,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 UnauthorizedRequestError({ message: "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 +652,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 +674,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 +695,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 +744,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 +752,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 +778,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 UnauthorizedRequestError({ message: "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 +806,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 +814,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 +873,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 +881,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,
}, },
@@ -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;
} }
+78 -69
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: {
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']
}
}, },
{ workspace: {
timestamps: true 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<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema); const ServiceTokenData = model<IServiceTokenData>(
"ServiceTokenData",
serviceTokenDataSchema
);
export default ServiceTokenData; export default ServiceTokenData;
+50 -47
View File
@@ -1,15 +1,15 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireSecretsAuth, requireSecretsAuth,
validateRequest, validateRequest,
} from '../../middleware'; } from "../../middleware";
import { validateClientForSecrets } from '../../validation'; import { validateClientForSecrets } from "../../validation";
import { query, body } from 'express-validator'; import { query, body } from "express-validator";
import { secretsController } from '../../controllers/v2'; import { secretsController } from "../../controllers/v2";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
@@ -21,11 +21,11 @@ import {
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
} from '../../variables'; } from "../../variables";
import { BatchSecretRequest } from '../../types/secret'; import { BatchSecretRequest } from "../../types/secret";
router.post( router.post(
'/batch', "/batch",
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
@@ -35,12 +35,13 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
}), }),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('folderId').default('root').isString().trim(), body("folderId").default("root").isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('requests') body("secretPath").optional().isString().trim(),
body("requests")
.exists() .exists()
.custom(async (requests: BatchSecretRequest[], { req }) => { .custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) { if (Array.isArray(requests)) {
@@ -65,17 +66,18 @@ router.post(
); );
router.post( router.post(
'/', "/",
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('folderId').default('root').isString().trim(), body("folderId").default("root").isString().trim(),
body('secrets') body("secretPath").optional().isString().trim(),
body("secrets")
.exists() .exists()
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: create multiple secrets // case: create multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.type || !secret.type ||
@@ -85,16 +87,16 @@ router.post(
!secret.secretKeyCiphertext || !secret.secretKeyCiphertext ||
!secret.secretKeyIV || !secret.secretKeyIV ||
!secret.secretKeyTag || !secret.secretKeyTag ||
typeof secret.secretValueCiphertext !== 'string' || typeof secret.secretValueCiphertext !== "string" ||
!secret.secretValueIV || !secret.secretValueIV ||
!secret.secretValueTag !secret.secretValueTag
) { ) {
throw new Error( throw new Error(
'secrets array must contain objects that have required secret properties' "secrets array must contain objects that have required secret properties"
); );
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === "object") {
// case: update 1 secret // case: update 1 secret
if ( if (
!value.type || !value.type ||
@@ -107,11 +109,11 @@ router.post(
!value.secretValueTag !value.secretValueTag
) { ) {
throw new Error( throw new Error(
'secrets object is missing required secret properties' "secrets object is missing required secret properties"
); );
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects'); throw new Error("secrets must be an object or an array of objects");
} }
return true; return true;
@@ -126,19 +128,20 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
router.get( router.get(
'/', "/",
query('workspaceId').exists().trim(), query("workspaceId").exists().trim(),
query('environment').exists().trim(), query("environment").exists().trim(),
query('tagSlugs'), query("tagSlugs"),
query('folderId').default('root').isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
@@ -150,34 +153,34 @@ router.get(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query', locationWorkspaceId: "query",
locationEnvironment: 'query', locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
router.patch( router.patch(
'/', "/",
body('secrets') body("secrets")
.exists() .exists()
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: update multiple secrets // case: update multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if (!secret.id) { if (!secret.id) {
throw new Error('Each secret must contain a ID property'); throw new Error("Each secret must contain a ID property");
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === "object") {
// case: update 1 secret // case: update 1 secret
if (!value.id) { if (!value.id) {
throw new Error('secret must contain a ID property'); throw new Error("secret must contain a ID property");
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects'); throw new Error("secrets must be an object or an array of objects");
} }
return true; return true;
@@ -198,21 +201,21 @@ router.patch(
); );
router.delete( router.delete(
'/', "/",
body('secretIds') body("secretIds")
.exists() .exists()
.custom((value) => { .custom((value) => {
// case: delete 1 secret // case: delete 1 secret
if (typeof value === 'string') return true; if (typeof value === "string") return true;
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: delete multiple secrets // case: delete multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
return value.every((id: string) => typeof id === 'string'); return value.every((id: string) => typeof id === "string");
} }
throw new Error('secretIds must be a string or an array of strings'); throw new Error("secretIds must be a string or an array of strings");
}) })
.not() .not()
.isEmpty(), .isEmpty(),
+63 -56
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;
+142 -137
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;
+29 -1
View File
@@ -1,5 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { TFolderSchema } from "../models/folder"; import { Types } from "mongoose";
import Folder, { TFolderSchema } from "../models/folder";
type TAppendFolderDTO = { type TAppendFolderDTO = {
folderName: string; folderName: string;
@@ -174,6 +175,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;
@@ -187,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
} }
return segment; return segment;
}; };
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";
};
+154 -156
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] * Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.secretName - name of secret to create * @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for * @param {String} obj.environment - environment in workspace
* @param {String} obj.environment - environment in workspace to create secret for * @param {AuthData} obj.authData - authentication data on request
* @param {'shared' | 'personal'} obj.type - type of secret * @returns
* @param {AuthData} obj.authData - authentication data on request */
* @returns static async getSecrets(getSecretsParams: GetSecretsParams) {
*/ return await getSecretsHelper(getSecretsParams);
static async createSecret(createSecretParams: CreateSecretParams) { }
return await createSecretHelper(createSecretParams);
}
/** /**
* Get secrets for workspace with id [workspaceId] and environment [environment] * Get secret with name [secretName]
* @param {Object} obj * @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace * @param {String} obj.secretName - name of secret to get
* @param {String} obj.environment - environment in workspace * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {AuthData} obj.authData - authentication data on request * @param {String} obj.environment - environment in workspace that secret belongs to
* @returns * @param {'shared' | 'personal'} obj.type - type of secret
*/ * @param {AuthData} obj.authData - authentication data on request
static async getSecrets(getSecretsParams: GetSecretsParams) { * @returns
return await getSecretsHelper(getSecretsParams); */
} static async getSecret(getSecretParams: GetSecretParams) {
// TODO(akhilmhdh) The one above is diff. Change this to some other name
return await getSecretHelper(getSecretParams);
}
/** /**
* Get secret with name [secretName] * Update secret with name [secretName]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.secretName - name of secret to get * @param {String} obj.secretName - name of secret to update
* @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 {String} obj.secretValueCiphertext - ciphertext of secret value
* @returns * @param {String} obj.secretValueIV - IV of secret value
*/ * @param {String} obj.secretValueTag - tag of secret value
static async getSecret(getSecretParams: GetSecretParams) { * @param {AuthData} obj.authData - authentication data on request
return await getSecretHelper(getSecretParams); * @returns
} */
static async updateSecret(updateSecretParams: UpdateSecretParams) {
return await updateSecretHelper(updateSecretParams);
}
/** /**
* Update secret with name [secretName] * Delete secret with name [secretName]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.secretName - name of secret to update * @param {String} obj.secretName - name of secret to delete
* @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 {String} obj.secretValueCiphertext - ciphertext of secret value * @param {AuthData} obj.authData - authentication data on request
* @param {String} obj.secretValueIV - IV of secret value * @returns
* @param {String} obj.secretValueTag - tag of secret value */
* @param {AuthData} obj.authData - authentication data on request static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
* @returns return await deleteSecretHelper(deleteSecretParams);
*/ }
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;
+24 -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,21 @@ 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 () => {
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(),
}); });
@@ -121,7 +121,7 @@ export default function NavHeader({
<span className="text-sm font-semibold text-bunker-300">{name}</span> <span className="text-sm font-semibold text-bunker-300">{name}</span>
) : ( ) : (
<Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}> <Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}>
<a className="text-sm font-semibold capitalize text-primary/80 hover:text-primary"> <a className="text-sm font-semibold text-primary/80 hover:text-primary">
{name === 'root' ? selectedEnv?.name : name} {name === 'root' ? selectedEnv?.name : name}
</a> </a>
</Link> </Link>
@@ -3,6 +3,7 @@ export type ServiceToken = {
name: string; name: string;
workspace: string; workspace: string;
environment: string; environment: string;
secretPath: string;
user: string; user: string;
expiresAt: string; expiresAt: string;
createdAt: string; createdAt: string;
@@ -15,6 +16,7 @@ export type CreateServiceTokenDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
expiresIn: number; expiresIn: number;
secretPath: string;
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
@@ -23,17 +23,20 @@ export const FolderSection = ({
{folders {folders
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase())) .filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map(({ id, name }) => ( .map(({ id, name }) => (
<tr key={id} className="group flex flex-row items-center hover:bg-mineshaft-700 cursor-default"> <tr
<td className="flex h-10 w-10 items-center justify-center border-none px-4 ml-0.5"> key={id}
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
>
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFolder} className="text-primary-700" /> <FontAwesomeIcon icon={faFolder} className="text-primary-700" />
</td> </td>
<td <td
colSpan={2} colSpan={2}
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis uppercase lg:min-w-[240px] xl:min-w-[280px]" className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
style={{ paddingTop: '0', paddingBottom: '0' }} style={{ paddingTop: '0', paddingBottom: '0' }}
> >
<div <div
className="flex-grow p-2 cursor-default" className="flex-grow cursor-default p-2"
onKeyDown={() => null} onKeyDown={() => null}
tabIndex={0} tabIndex={0}
role="button" role="button"
@@ -217,7 +217,8 @@ export const ProjectSettingsPage = () => {
environment, environment,
expiresIn, expiresIn,
name, name,
permissions permissions,
secretPath
}: CreateServiceToken) => { }: CreateServiceToken) => {
// type guard // type guard
if (!latestFileKey) return ''; if (!latestFileKey) return '';
@@ -241,6 +242,7 @@ export const ProjectSettingsPage = () => {
iv, iv,
tag, tag,
environment, environment,
secretPath,
expiresIn: Number(expiresIn), expiresIn: Number(expiresIn),
name, name,
workspaceId: workspaceID, workspaceId: workspaceID,
@@ -42,8 +42,9 @@ const apiTokenExpiry = [
]; ];
const createServiceTokenSchema = yup.object({ const createServiceTokenSchema = yup.object({
name: yup.string().required().label('Service Token Name'), name: yup.string().max(100).required().label('Service Token Name'),
environment: yup.string().required().label('Environment'), environment: yup.string().max(50).required().label('Environment'),
secretPath: yup.string().required().default('/').label('Secret Path'),
expiresIn: yup.string().optional().label('Service Token Expiration'), expiresIn: yup.string().optional().label('Service Token Expiration'),
permissions: yup permissions: yup
.object() .object()
@@ -189,6 +190,22 @@ export const ServiceTokenSection = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
isError={Boolean(error)}
helperText="Tokens can be scoped to a folder path. Default path is /"
errorText={error?.message}
>
<Input {...field} placeholder="Provide a path, default is /" />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
name="expiresIn" name="expiresIn"
@@ -317,6 +334,7 @@ export const ServiceTokenSection = ({
<Tr> <Tr>
<Th>Token Name</Th> <Th>Token Name</Th>
<Th>Environment</Th> <Th>Environment</Th>
<Th>Secret Path</Th>
<Th>Valid Until</Th> <Th>Valid Until</Th>
<Th aria-label="button" /> <Th aria-label="button" />
</Tr> </Tr>
@@ -328,6 +346,7 @@ export const ServiceTokenSection = ({
<Tr key={row._id}> <Tr key={row._id}>
<Td>{row.name}</Td> <Td>{row.name}</Td>
<Td>{row.environment}</Td> <Td>{row.environment}</Td>
<Td>{row.secretPath}</Td>
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td> <Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
<Td className="flex items-center justify-end"> <Td className="flex items-center justify-end">
<IconButton <IconButton