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

This commit is contained in:
akhilmhdh
2023-06-09 23:41:22 +05:30
parent 00fae0023a
commit 6cb0a20675
11 changed files with 892 additions and 719 deletions

View File

@@ -1,29 +1,25 @@
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import { Request, Response } from "express";
import crypto from "crypto";
import bcrypt from "bcrypt";
import { User, ServiceAccount, ServiceTokenData } from "../../models";
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
import {
User,
ServiceAccount,
ServiceTokenData
} from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN
} from '../../variables';
import { getSaltRounds } from '../../config';
import { BadRequestError } from '../../utils/errors';
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
} from "../../variables";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
/**
* Return service token data associated with service token on request
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
@@ -49,98 +45,110 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
}
*/
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({
message: 'Failed accepted client validation for service token data'
if (!(req.authData.authPayload instanceof ServiceTokenData))
throw BadRequestError({
message: "Failed accepted client validation for service token data",
});
const serviceTokenData = await ServiceTokenData
.findById(req.authData.authPayload._id)
.select('+encryptedKey +iv +tag')
.populate('user');
const serviceTokenData = await ServiceTokenData.findById(
req.authData.authPayload._id
)
.select("+encryptedKey +iv +tag")
.populate("user");
return res.status(200).json(serviceTokenData);
}
return res.status(200).json(serviceTokenData);
};
/**
* Create new service token data for workspace with id [workspaceId] and
* environment [environment].
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
let serviceTokenData;
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
secretPath,
permissions,
} = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt;
if (expiresIn) {
expiresAt = new Date()
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
if (
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User
) {
user = req.authData.authPayload._id;
}
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
req.authData.authPayload instanceof ServiceAccount
) {
serviceAccount = req.authData.authPayload._id;
}
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
secretPath,
permissions,
}).save();
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
return res.status(200).send({
serviceToken,
serviceTokenData
});
}
if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({
serviceToken,
serviceTokenData,
});
};
/**
* Delete service token data with id [serviceTokenDataId].
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params;
const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
serviceTokenDataId
);
return res.status(200).send({
serviceTokenData
});
}
return res.status(200).send({
serviceTokenData,
});
};

View File

@@ -1,183 +1,184 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
SecretService,
TelemetryService,
EventService
} from '../../services';
import { eventPushSecrets } from '../../events';
import { getAuthDataPayloadIdObj } from '../../utils/auth';
import { BadRequestError } from '../../utils/errors';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { SecretService, EventService } from "../../services";
import { eventPushSecrets } from "../../events";
/**
* Get secrets for workspace with id [workspaceId] and environment
* [environment]
* @param req
* @param res
* @param req
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
authData: req.authData
});
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secrets
});
}
return res.status(200).send({
secrets,
});
};
/**
* Get secret with name [secretName]
* @param req
* @param res
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const type = req.query.type as 'shared' | 'personal' | undefined;
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData
});
return res.status(200).send({
secret
});
}
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secret,
});
};
/**
* Create secret with name [secretName]
* @param req
* @param res
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {})
});
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/",
} = req.body;
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
...(secretCommentCiphertext && secretCommentIV && secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
}
: {}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex,
});
};
/**
* Update secret with name [secretName]
* @param req
* @param res
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag
} = req.body;
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath = "/",
} = req.body;
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
});
return res.status(200).send({
secret
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret,
});
};
/**
* Delete secret with name [secretName]
* @param req
* @param res
* @param req
* @param res
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData
});
const { secretName } = req.params;
const { workspaceId, environment, type, secretPath = "/" } = req.body;
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretPath,
});
return res.status(200).send({
secret
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret,
});
};

View File

@@ -1,19 +1,24 @@
import { Types } from 'mongoose';
import { Types } from "mongoose";
import {
CreateSecretParams,
GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams,
} from '../interfaces/services/SecretService';
import { Secret, ISecret, SecretBlindIndexData } from '../models';
import { SecretVersion } from '../ee/models';
} from "../interfaces/services/SecretService";
import {
Secret,
ISecret,
SecretBlindIndexData,
ServiceTokenData,
} from "../models";
import { SecretVersion } from "../ee/models";
import {
BadRequestError,
SecretNotFoundError,
SecretBlindIndexDataNotFoundError,
InternalServerError,
} from '../utils/errors';
} from "../utils/errors";
import {
SECRET_PERSONAL,
SECRET_SHARED,
@@ -24,20 +29,44 @@ import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
} from '../variables';
import crypto from 'crypto';
import * as argon2 from 'argon2';
} from "../variables";
import crypto from "crypto";
import * as argon2 from "argon2";
import {
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8,
} from '../utils/crypto';
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
import { TelemetryService } from '../services';
import { EESecretService, EELogService } from '../ee/services';
} from "../utils/crypto";
import { getEncryptionKey, client, getRootEncryptionKey } from "../config";
import { TelemetryService } from "../services";
import { EESecretService, EELogService } from "../ee/services";
import {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj,
} from '../utils/auth';
} from "../utils/auth";
import Folder from "../models/folder";
import { getFolderByPath } from "../services/FolderService";
export const getFolderIdFromServiceToken = async (
workspaceId: Types.ObjectId | string,
environment: string,
secretPath: string
) => {
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders) {
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
} else {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw new Error("Folder not found");
}
return folder.id;
}
return "root";
};
/**
* Create secret blind index data containing encrypted blind index [salt]
@@ -46,12 +75,12 @@ import {
* @param {Types.ObjectId} obj.workspaceId
*/
export const createSecretBlindIndexDataHelper = async ({
workspaceId
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
// initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64');
const salt = crypto.randomBytes(16).toString("base64");
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
@@ -99,7 +128,7 @@ export const createSecretBlindIndexDataHelper = async ({
* @returns
*/
export const getSecretBlindIndexSaltHelper = async ({
workspaceId
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
@@ -108,7 +137,7 @@ export const getSecretBlindIndexSaltHelper = async ({
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId,
}).select('+algorithm +keyEncoding');
}).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
@@ -136,7 +165,7 @@ export const getSecretBlindIndexSaltHelper = async ({
}
throw InternalServerError({
message: 'Failed to obtain workspace salt needed for secret blind indexing',
message: "Failed to obtain workspace salt needed for secret blind indexing",
});
};
@@ -148,8 +177,8 @@ export const getSecretBlindIndexSaltHelper = async ({
* @param {String} obj.salt - base64-salt
*/
export const generateSecretBlindIndexWithSaltHelper = async ({
secretName,
salt
secretName,
salt,
}: {
secretName: string;
salt: string;
@@ -158,14 +187,14 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
const secretBlindIndex = (
await argon2.hash(secretName, {
type: argon2.argon2id,
salt: Buffer.from(salt, 'base64'),
salt: Buffer.from(salt, "base64"),
saltLength: 16, // default 16 bytes
memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32,
parallelism: 1,
raw: true,
})
).toString('base64');
).toString("base64");
return secretBlindIndex;
};
@@ -178,8 +207,8 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
export const generateSecretBlindIndexHelper = async ({
secretName,
workspaceId
secretName,
workspaceId,
}: {
secretName: string;
workspaceId: Types.ObjectId;
@@ -190,7 +219,7 @@ export const generateSecretBlindIndexHelper = async ({
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId,
}).select('+algorithm +keyEncoding');
}).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
@@ -231,9 +260,9 @@ export const generateSecretBlindIndexHelper = async ({
return secretBlindIndex;
}
throw InternalServerError({
message: 'Failed to generate secret blind index'
message: "Failed to generate secret blind index",
});
};
@@ -262,23 +291,37 @@ export const createSecretHelper = async ({
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folderId,
secretPath = "/",
}: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
});
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw new Error("Folder Permission Denied");
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
const exists = await Secret.exists({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
});
if (exists)
throw BadRequestError({
message: 'Failed to create secret that already exists',
message: "Failed to create secret that already exists",
});
if (type === SECRET_PERSONAL) {
@@ -287,6 +330,7 @@ export const createSecretHelper = async ({
const exists = await Secret.exists({
secretBlindIndex,
folder: folderId,
workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED,
});
@@ -294,7 +338,7 @@ export const createSecretHelper = async ({
if (!exists)
throw BadRequestError({
message:
'Failed to create personal secret override for no corresponding shared secret',
"Failed to create personal secret override for no corresponding shared secret",
});
}
@@ -325,6 +369,7 @@ export const createSecretHelper = async ({
version: secret.version,
workspace: secret.workspace,
type,
folder: folderId,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
@@ -372,7 +417,7 @@ export const createSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
event: "secrets added",
distinctId: await TelemetryService.getDistinctId({
authData,
}),
@@ -380,6 +425,7 @@ export const createSecretHelper = async ({
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent,
},
@@ -398,16 +444,30 @@ export const createSecretHelper = async ({
* @returns
*/
export const getSecretsHelper = async ({
workspaceId,
environment,
authData
workspaceId,
environment,
authData,
secretPath = "/",
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw new Error("Folder Permission Denied");
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
// get personal secrets first
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData),
});
@@ -417,6 +477,7 @@ export const getSecretsHelper = async ({
await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type: SECRET_SHARED,
secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex),
@@ -445,7 +506,7 @@ export const getSecretsHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({
authData,
}),
@@ -453,6 +514,7 @@ export const getSecretsHelper = async ({
numberOfSecrets: secrets.length,
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent,
},
@@ -478,18 +540,32 @@ export const getSecretHelper = async ({
environment,
type,
authData,
secretPath = "/",
}: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
});
let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw new Error("Folder Permission Denied");
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
// try getting personal secret first (if exists)
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
});
@@ -501,6 +577,7 @@ export const getSecretHelper = async ({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type: SECRET_SHARED,
});
}
@@ -528,7 +605,7 @@ export const getSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets pull',
event: "secrets pull",
distinctId: await TelemetryService.getDistinctId({
authData,
}),
@@ -536,6 +613,7 @@ export const getSecretHelper = async ({
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent,
},
@@ -568,6 +646,7 @@ export const updateSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
}: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
@@ -575,6 +654,18 @@ export const updateSecretHelper = async ({
});
let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw new Error("Folder Permission Denied");
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
if (type === SECRET_SHARED) {
// case: update shared secret
@@ -583,6 +674,7 @@ export const updateSecretHelper = async ({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type,
},
{
@@ -604,6 +696,7 @@ export const updateSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId),
environment,
type,
folder: folderId,
...getAuthDataPayloadUserObj(authData),
},
{
@@ -624,6 +717,7 @@ export const updateSecretHelper = async ({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
folder: folderId,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
@@ -672,7 +766,7 @@ export const updateSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({
authData,
}),
@@ -680,6 +774,7 @@ export const updateSecretHelper = async ({
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent,
},
@@ -705,12 +800,26 @@ export const deleteSecretHelper = async ({
environment,
type,
authData,
secretPath = "/",
}: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
});
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw new Error("Folder Permission Denied");
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
let secrets: ISecret[] = [];
let secret: ISecret | null = null;
@@ -719,6 +828,7 @@ export const deleteSecretHelper = async ({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
});
secret = await Secret.findOneAndDelete({
@@ -726,16 +836,19 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
folder: folderId,
});
await Secret.deleteMany({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
});
} else {
secret = await Secret.findOneAndDelete({
secretBlindIndex,
folder: folderId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
@@ -782,7 +895,7 @@ export const deleteSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({
authData,
}),
@@ -790,6 +903,7 @@ export const deleteSecretHelper = async ({
numberOfSecrets: secrets.length,
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent,
},
@@ -800,4 +914,4 @@ export const deleteSecretHelper = async ({
secrets,
secret,
};
};
};

View File

@@ -5,7 +5,6 @@ export interface CreateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
type: "shared" | "personal";
authData: AuthData;
secretKeyCiphertext: string;
@@ -17,17 +16,20 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretPath: string;
}
export interface GetSecretsParams {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
authData: AuthData;
}
export interface GetSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
secretPath: string;
environment: string;
type?: "shared" | "personal";
authData: AuthData;
@@ -42,7 +44,7 @@ export interface UpdateSecretParams {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
folderId?: string;
secretPath: string;
}
export interface DeleteSecretParams {
@@ -51,4 +53,5 @@ export interface DeleteSecretParams {
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretPath: string;
}

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 {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
environment: string;
user: Types.ObjectId;
serviceAccount: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
permissions: string[];
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
environment: string;
user: Types.ObjectId;
serviceAccount: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
secretPath: string;
permissions: string[];
}
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{
name: {
type: String,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
},
encryptedKey: {
type: String,
select: false
},
iv: {
type: String,
select: false
},
tag: {
type: String,
select: false
},
permissions: {
type: [String],
enum: ['read', 'write'],
default: ['read']
}
},
{
timestamps: true
}
{
name: {
type: String,
required: true,
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true,
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: "ServiceAccount",
},
lastUsed: {
type: Date,
},
expiresAt: {
type: Date,
},
secretHash: {
type: String,
required: true,
select: false,
},
encryptedKey: {
type: String,
select: false,
},
iv: {
type: String,
select: false,
},
tag: {
type: String,
select: false,
},
permissions: {
type: [String],
enum: ["read", "write"],
default: ["read"],
},
secretPath: {
type: String,
default: "/",
required: true,
},
},
{
timestamps: true,
}
);
const ServiceTokenData = model<IServiceTokenData>(
"ServiceTokenData",
serviceTokenDataSchema
);
const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema);
export default ServiceTokenData;

View File

@@ -1,72 +1,79 @@
import express from 'express';
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
requireServiceTokenDataAuth,
validateRequest
} from '../../middleware';
import { param, body } from 'express-validator';
requireAuth,
requireWorkspaceAuth,
requireServiceTokenDataAuth,
validateRequest,
} from "../../middleware";
import { param, body } from "express-validator";
import {
ADMIN,
MEMBER,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN
} from '../../variables';
import { serviceTokenDataController } from '../../controllers/v2';
ADMIN,
MEMBER,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
} from "../../variables";
import { serviceTokenDataController } from "../../controllers/v2";
router.get(
'/',
requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
}),
serviceTokenDataController.getServiceTokenData
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN],
}),
serviceTokenDataController.getServiceTokenData
);
router.post(
'/',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
body('name').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('encryptedKey').exists().isString().trim(),
body('iv').exists().isString().trim(),
body('tag').exists().isString().trim(),
body('expiresIn').exists().isNumeric(), // measured in ms
body('permissions').isArray({ min: 1 }).custom((value: string[]) => {
const allowedPermissions = ['read', 'write'];
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
if (invalidValues.length > 0) {
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`);
}
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
}),
body("name").exists().isString().trim(),
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("encryptedKey").exists().isString().trim(),
body("iv").exists().isString().trim(),
body("secretPath").isString().default("/").trim(),
body("tag").exists().isString().trim(),
body("expiresIn").exists().isNumeric(), // measured in ms
body("permissions")
.isArray({ min: 1 })
.custom((value: string[]) => {
const allowedPermissions = ["read", "write"];
const invalidValues = value.filter(
(v) => !allowedPermissions.includes(v)
);
if (invalidValues.length > 0) {
throw new Error(
`permissions contains invalid values: ${invalidValues.join(", ")}`
);
}
return true
return true;
}),
validateRequest,
serviceTokenDataController.createServiceTokenData
validateRequest,
serviceTokenDataController.createServiceTokenData
);
router.delete(
'/:serviceTokenDataId',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('serviceTokenDataId').exists().trim(),
validateRequest,
serviceTokenDataController.deleteServiceTokenData
"/:serviceTokenDataId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("serviceTokenDataId").exists().trim(),
validateRequest,
serviceTokenDataController.deleteServiceTokenData
);
export default router;
export default router;

View File

@@ -1,157 +1,162 @@
import express from 'express';
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { secretsController } from '../../controllers/v3';
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3";
import {
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
ADMIN,
MEMBER,
PERMISSION_WRITE_SECRETS,
SECRET_SHARED,
SECRET_PERSONAL,
PERMISSION_READ_SECRETS
} from '../../variables';
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
ADMIN,
MEMBER,
PERMISSION_WRITE_SECRETS,
SECRET_SHARED,
SECRET_PERSONAL,
PERMISSION_READ_SECRETS,
} from "../../variables";
router.get(
'/',
query('workspaceId').exists().isString().trim(),
query('environment').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.getSecrets
"/",
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.getSecrets
);
router.post(
'/:secretName',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretKeyCiphertext').exists().isString().trim(),
body('secretKeyIV').exists().isString().trim(),
body('secretKeyTag').exists().isString().trim(),
body('secretValueCiphertext').exists().isString().trim(),
body('secretValueIV').exists().isString().trim(),
body('secretValueTag').exists().isString().trim(),
body('secretCommentCiphertext').optional().isString().trim(),
body('secretCommentIV').optional().isString().trim(),
body('secretCommentTag').optional().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.createSecret
"/:secretName",
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body("secretKeyCiphertext").exists().isString().trim(),
body("secretKeyIV").exists().isString().trim(),
body("secretKeyTag").exists().isString().trim(),
body("secretValueCiphertext").exists().isString().trim(),
body("secretValueIV").exists().isString().trim(),
body("secretValueTag").exists().isString().trim(),
body("secretCommentCiphertext").optional().isString().trim(),
body("secretCommentIV").optional().isString().trim(),
body("secretCommentTag").optional().isString().trim(),
body("secretPath").default("/").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.createSecret
);
router.get(
'/:secretName',
param('secretName').exists().isString().trim(),
query('workspaceId').exists().isString().trim(),
query('environment').exists().isString().trim(),
query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.getSecretByName
"/:secretName",
param("secretName").exists().isString().trim(),
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(),
query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.getSecretByName
);
router.patch(
'/:secretName',
param('secretName').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretValueCiphertext').exists().isString().trim(),
body('secretValueIV').exists().isString().trim(),
body('secretValueTag').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.updateSecretByName
"/:secretName",
param("secretName").exists().isString().trim(),
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body("secretValueCiphertext").exists().isString().trim(),
body("secretValueIV").exists().isString().trim(),
body("secretValueTag").exists().isString().trim(),
body("secretPath").default("/").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.updateSecretByName
);
router.delete(
'/:secretName',
param('secretName').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.deleteSecretByName
"/:secretName",
param("secretName").exists().isString().trim(),
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("secretPath").default("/").isString().trim(),
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.deleteSecretByName
);
export default router;
export default router;

View File

@@ -174,6 +174,11 @@ export const searchByFolderIdWithDir = (
// to get folder of a path given
// Like /frontend/folder#1
export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
// corner case when its just / return root
if (searchPath === "/") {
return folders.id === "root" ? folders : undefined;
}
const path = searchPath.split("/").filter(Boolean);
const queue = [folders];
let segment: TFolderSchema | undefined;

View File

@@ -1,171 +1,169 @@
import { Types } from 'mongoose';
import { Types } from "mongoose";
import { ISecret } from "../models";
import {
ISecret
} from '../models';
CreateSecretParams,
GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams,
} from "../interfaces/services/SecretService";
import {
CreateSecretParams,
GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper
} from '../helpers/secrets';
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper,
} from "../helpers/secrets";
class SecretService {
/**
* Create secret blind index data containing encrypted blind index salt
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Buffer} obj.salt - 16-byte random salt
* @param {Types.ObjectId} obj.workspaceId
*/
static async createSecretBlindIndexData({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await createSecretBlindIndexDataHelper({
workspaceId,
});
}
/**
* Create secret blind index data containing encrypted blind index salt
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Buffer} obj.salt - 16-byte random salt
* @param {Types.ObjectId} obj.workspaceId
*/
static async createSecretBlindIndexData({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await createSecretBlindIndexDataHelper({
workspaceId
});
}
/**
* Get secret blind index salt for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
static async getSecretBlindIndexSalt({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await getSecretBlindIndexSaltHelper({
workspaceId,
});
}
/**
* Get secret blind index salt for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
static async getSecretBlindIndexSalt({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) {
return await getSecretBlindIndexSaltHelper({
workspaceId
});
}
/**
* Generate blind index for secret with name [secretName]
* and salt [salt]
* @param {Object} obj
* @param {Object} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
static async generateSecretBlindIndexWithSalt({
secretName,
salt,
}: {
secretName: string;
salt: string;
}) {
return await generateSecretBlindIndexWithSaltHelper({
secretName,
salt,
});
}
/**
* Generate blind index for secret with name [secretName]
* and salt [salt]
* @param {Object} obj
* @param {Object} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
static async generateSecretBlindIndexWithSalt({
secretName,
salt
}: {
secretName: string;
salt: string;
}) {
return await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
}
/**
* Create and return blind index for secret with
* name [secretName] part of workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
static async generateSecretBlindIndex({
secretName,
workspaceId,
}: {
secretName: string;
workspaceId: Types.ObjectId;
}) {
return await generateSecretBlindIndexHelper({
secretName,
workspaceId,
});
}
/**
* Create and return blind index for secret with
* name [secretName] part of workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
static async generateSecretBlindIndex({
secretName,
workspaceId,
}: {
secretName: string;
workspaceId: Types.ObjectId;
}) {
return await generateSecretBlindIndexHelper({
secretName,
workspaceId
});
}
/**
* Create secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to create
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
* @param {String} obj.environment - environment in workspace to create secret for
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async createSecret(createSecretParams: CreateSecretParams) {
return await createSecretHelper(createSecretParams);
}
/**
* Create secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to create
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
* @param {String} obj.environment - environment in workspace to create secret for
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async createSecret(createSecretParams: CreateSecretParams) {
return await createSecretHelper(createSecretParams);
}
/**
* Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment in workspace
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async getSecrets(getSecretsParams: GetSecretsParams) {
return await getSecretsHelper(getSecretsParams);
}
/**
* Get secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to get
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async getSecret(getSecretParams: GetSecretParams) {
return await getSecretHelper(getSecretParams);
}
/**
* Update secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to update
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
* @param {String} obj.secretValueIV - IV of secret value
* @param {String} obj.secretValueTag - tag of secret value
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async updateSecret(updateSecretParams: UpdateSecretParams) {
return await updateSecretHelper(updateSecretParams);
}
/**
* Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment in workspace
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async getSecrets(getSecretsParams: GetSecretsParams) {
return await getSecretsHelper(getSecretsParams);
}
/**
* Delete secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to delete
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
return await deleteSecretHelper(deleteSecretParams);
}
/**
* Get secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to get
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async getSecret(getSecretParams: GetSecretParams) {
// TODO(akhilmhdh) The one above is diff. Change this to some other name
return await getSecretHelper(getSecretParams);
}
/**
* Update secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to update
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
* @param {String} obj.secretValueIV - IV of secret value
* @param {String} obj.secretValueTag - tag of secret value
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async updateSecret(updateSecretParams: UpdateSecretParams) {
return await updateSecretHelper(updateSecretParams);
}
/**
* Delete secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to delete
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
return await deleteSecretHelper(deleteSecretParams);
}
}
export default SecretService;
export default SecretService;

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import crypto from "crypto";
import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
@@ -11,6 +12,7 @@ import {
Bot,
BackupPrivateKey,
IntegrationAuth,
ServiceTokenData,
} from "../../models";
import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -64,7 +66,7 @@ export const backfillSecretVersions = async () => {
),
});
}
console.log("Migration: Secret version migration v1 complete")
console.log("Migration: Secret version migration v1 complete");
};
/**
@@ -380,13 +382,15 @@ export const backfillSecretFolders = async () => {
});
const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => {
const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv] ? groupSnapByEnv[snapEnv].map(secretVersion => secretVersion._id) : []
const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv]
? groupSnapByEnv[snapEnv].map((secretVersion) => secretVersion._id)
: [];
return {
...secSnapshot.toObject({ virtuals: false }),
_id: new Types.ObjectId(),
environment: snapEnv,
secretVersions: secretIdsOfEnvGroup,
}
};
});
await SecretSnapshot.insertMany(newSnapshots);
@@ -402,5 +406,22 @@ export const backfillSecretFolders = async () => {
.limit(50);
}
console.log("Migration: Folder migration v1 complete")
console.log("Migration: Folder migration v1 complete");
};
export const backfillServiceToken = async () => {
// Back fill because tags were missing in secret versions
await ServiceTokenData.updateMany(
{
secretPath: {
$exists: false,
},
},
{
$set: {
secretPath: "/",
},
}
);
console.log("Migration: Service token migration v1 complete");
};

View File

@@ -1,30 +1,31 @@
import * as Sentry from '@sentry/node';
import { DatabaseService, TelemetryService } from '../../services';
import { setTransporter } from '../../helpers/nodemailer';
import { EELicenseService } from '../../ee/services';
import { initSmtp } from '../../services/smtp';
import { createTestUserForDevelopment } from '../addDevelopmentUser';
import * as Sentry from "@sentry/node";
import { DatabaseService, TelemetryService } from "../../services";
import { setTransporter } from "../../helpers/nodemailer";
import { EELicenseService } from "../../ee/services";
import { initSmtp } from "../../services/smtp";
import { createTestUserForDevelopment } from "../addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires
import { validateEncryptionKeysConfig } from './validateConfig';
import { validateEncryptionKeysConfig } from "./validateConfig";
import {
backfillSecretVersions,
backfillBots,
backfillSecretBlindIndexData,
backfillEncryptionMetadata,
backfillSecretFolders,
} from './backfillData';
backfillServiceToken,
} from "./backfillData";
import {
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts,
} from './reencryptData';
} from "./reencryptData";
import {
getNodeEnv,
getMongoURL,
getSentryDSN,
getClientSecretGoogle,
getClientIdGoogle,
} from '../../config';
import { initializePassport } from '../auth';
} from "../../config";
import { initializePassport } from "../auth";
/**
* Prepare Infisical upon startup. This includes tasks like:
@@ -75,6 +76,7 @@ export const setup = async () => {
await backfillSecretBlindIndexData();
await backfillEncryptionMetadata();
await backfillSecretFolders();
await backfillServiceToken();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
@@ -85,7 +87,7 @@ export const setup = async () => {
Sentry.init({
dsn: await getSentryDSN(),
tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === 'production' ? false : true,
debug: (await getNodeEnv()) === "production" ? false : true,
environment: await getNodeEnv(),
});