mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Complete first iteration of CRUD secrets operations by name
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Bot, BotKey } from '../../models';
|
import { Bot, BotKey } from '../../models';
|
||||||
import { createBot } from '../../helpers/bot';
|
import { createBot } from '../../helpers/bot';
|
||||||
@@ -29,7 +30,7 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
|||||||
// -> create a new bot and return it
|
// -> create a new bot and return it
|
||||||
bot = await createBot({
|
bot = await createBot({
|
||||||
name: 'Infisical Bot',
|
name: 'Infisical Bot',
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import to from 'await-to-js';
|
|||||||
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 } from '../../models';
|
||||||
import { IAction } from '../../ee/models';
|
import { IAction, SecretVersion } from '../../ee/models';
|
||||||
import {
|
import {
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const batchSecrets = async (req: Request, res: Response) => {
|
export const batchSecrets = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
const channel = getChannelFromUserAgent(req.headers['user-agent']);
|
const channel = getChannelFromUserAgent(req.headers['user-agent']);
|
||||||
const postHogClient = TelemetryService.getPostHogClient();
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
await Secret.bulkWrite(updateOperations);
|
await Secret.bulkWrite(updateOperations);
|
||||||
|
|
||||||
const secretVersions = updateSecrets.map((u) => ({
|
const secretVersions = updateSecrets.map((u) => new SecretVersion({
|
||||||
secret: new Types.ObjectId(u._id),
|
secret: new Types.ObjectId(u._id),
|
||||||
version: listedSecretsObj[u._id.toString()].version,
|
version: listedSecretsObj[u._id.toString()].version,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
@@ -253,7 +254,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
const resObj: { [key: string]: ISecret[] | string[] } = {}
|
const resObj: { [key: string]: ISecret[] | string[] } = {}
|
||||||
@@ -422,13 +423,8 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag
|
||||||
secretCommentCiphertext,
|
}) => new SecretVersion({
|
||||||
secretCommentIV,
|
|
||||||
secretCommentTag,
|
|
||||||
tags
|
|
||||||
}) => ({
|
|
||||||
_id: new Types.ObjectId(),
|
|
||||||
secret: _id,
|
secret: _id,
|
||||||
version,
|
version,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -441,11 +437,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag
|
||||||
secretCommentCiphertext,
|
|
||||||
secretCommentIV,
|
|
||||||
secretCommentTag,
|
|
||||||
tags
|
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -471,7 +463,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = TelemetryService.getPostHogClient();
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
@@ -872,7 +864,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: key
|
workspaceId: new Types.ObjectId(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
const postHogClient = TelemetryService.getPostHogClient();
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
@@ -955,10 +947,6 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'delete secrets!!'
|
|
||||||
});
|
|
||||||
|
|
||||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||||
const toDelete = req.secrets.map((s: any) => s._id);
|
const toDelete = req.secrets.map((s: any) => s._id);
|
||||||
|
|
||||||
@@ -1012,8 +1000,8 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: key
|
workspaceId: new Types.ObjectId(key)
|
||||||
})
|
});
|
||||||
|
|
||||||
const postHogClient = TelemetryService.getPostHogClient();
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import { SecretService, TelemetryService } from '../../services';
|
||||||
Secret
|
import { getAuthDataPayloadIdObj } from '../../utils/auth';
|
||||||
} from '../../models';
|
import { BadRequestError } from '../../utils/errors';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { SecretService } from '../../services';
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: modularize argon2id
|
|
||||||
import * as argon2 from 'argon2';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get secrets for workspace with id [workspaceId] and environment
|
* Get secrets for workspace with id [workspaceId] and environment
|
||||||
@@ -17,41 +11,92 @@ import * as argon2 from 'argon2';
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getSecrets = async (req: Request, res: Response) => {
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
return res.status(200).send({
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const environment = req.query.environment as string;
|
||||||
|
|
||||||
|
const secrets = await SecretService.getSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
authData: req.authData
|
||||||
|
});
|
||||||
|
|
||||||
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pulled',
|
||||||
|
distinctId: TelemetryService.getDistinctId({
|
||||||
|
user: req.user,
|
||||||
|
serviceAccount: req.serviceAccount,
|
||||||
|
serviceTokenData: req.serviceTokenData
|
||||||
|
}),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.authData.authChannel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret with name [secretName] for workspace with id [workspaceId]
|
* Get secret with name [secretName]
|
||||||
* and environment [environment]
|
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getSecretByName = async (req: Request, res: Response) => {
|
export const getSecretByName = async (req: Request, res: Response) => {
|
||||||
const { secretName } = req.params;
|
const { secretName } = req.params;
|
||||||
const workspaceId = req.query.workspaceId as string;
|
const workspaceId = req.query.workspaceId as string;
|
||||||
const environment = req.query.workspaceId as string;
|
const environment = req.query.environment 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pull',
|
||||||
|
distinctId: TelemetryService.getDistinctId({
|
||||||
|
user: req.user,
|
||||||
|
serviceAccount: req.serviceAccount,
|
||||||
|
serviceTokenData: req.serviceTokenData
|
||||||
|
}),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.authData.authChannel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
secret
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret for workspace with id [workspaceId] and
|
* Create secret with name [secretName]
|
||||||
* environment [environment]
|
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createSecret = async (req: Request, res: Response) => {
|
export const createSecret = async (req: Request, res: Response) => {
|
||||||
// TODO: the middleware should've prevalidated that the
|
|
||||||
// workspace with id [workspaceId] has disabled E2EE
|
|
||||||
const { secretName } = req.params;
|
const { secretName } = req.params;
|
||||||
const {
|
const {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
value,
|
|
||||||
type,
|
type,
|
||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
@@ -61,66 +106,140 @@ export const createSecret = async (req: Request, res: Response) => {
|
|||||||
secretValueTag
|
secretValueTag
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const secretBlindIndex = await SecretService.createSecretBlindIndex({
|
const secret = await SecretService.createSecret({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
});
|
});
|
||||||
|
|
||||||
// // use workspace salt
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
// const randomBytes = crypto.randomBytes(16);
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
// // generate blind index
|
event: 'secrets added',
|
||||||
// // TODO 1: abstract away into create blind index function
|
distinctId: TelemetryService.getDistinctId({
|
||||||
// // TODO 2: create a get blind index function
|
user: req.user,
|
||||||
// const secretBlindIndex = (await argon2.hash(secretName, {
|
serviceAccount: req.serviceAccount,
|
||||||
// type: argon2.argon2id,
|
serviceTokenData: req.serviceTokenData
|
||||||
// salt: randomBytes,
|
}),
|
||||||
// saltLength: 16, // default 16 bytes
|
properties: {
|
||||||
// memoryCost: 65536, // default pool of 64 MiB per thread.
|
numberOfSecrets: 1,
|
||||||
// hashLength: 32,
|
environment,
|
||||||
// parallelism: 1,
|
workspaceId,
|
||||||
// raw: true
|
channel: req.authData.authChannel,
|
||||||
// })).toString('base64');
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
// const secret = await new Secret({
|
});
|
||||||
// workspace: new Types.ObjectId(workspaceId),
|
}
|
||||||
// environment,
|
|
||||||
// type,
|
const secretWithoutBlindIndex = secret.toObject();
|
||||||
// secretBlindIndex,
|
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||||
// secretKeyCiphertext,
|
|
||||||
// secretKeyIV,
|
|
||||||
// secretKeyTag,
|
|
||||||
// secretValueCiphertext,
|
|
||||||
// secretValueIV,
|
|
||||||
// secretValueTag
|
|
||||||
// }).save();
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
secret: secretWithoutBlindIndex
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update secret with name [secretName] in workspace with id [workspaceId]
|
* Update secret with name [secretName]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const updateSecretByName = async (req: Request, res: Response) => {
|
export const updateSecretByName = async (req: Request, res: Response) => {
|
||||||
const { secretName } = req.params;
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const secret = await SecretService.updateSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: TelemetryService.getDistinctId({
|
||||||
|
user: req.user,
|
||||||
|
serviceAccount: req.serviceAccount,
|
||||||
|
serviceTokenData: req.serviceTokenData
|
||||||
|
}),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.authData.authChannel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
secret
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete secret with name [secretName] in workspace with id [workspaceId]
|
* Delete secret with name [secretName]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const deleteSecretByName = async (req: Request, res: Response) => {
|
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { secret, secrets } = await SecretService.deleteSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData
|
||||||
|
});
|
||||||
|
|
||||||
|
const postHogClient = TelemetryService.getPostHogClient();
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: TelemetryService.getDistinctId({
|
||||||
|
user: req.user,
|
||||||
|
serviceAccount: req.serviceAccount,
|
||||||
|
serviceTokenData: req.serviceTokenData
|
||||||
|
}),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.authData.authChannel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
secret
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: secret.workspace.toString()
|
workspaceId: secret.workspace
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Log,
|
Log,
|
||||||
IAction
|
IAction
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an (audit) log
|
* Create an (audit) log
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Types } from 'mongoose';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ISecret
|
ISecret,
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import {
|
import {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
const takeSecretSnapshotHelper = async ({
|
const takeSecretSnapshotHelper = async ({
|
||||||
workspaceId
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
let secretSnapshot;
|
let secretSnapshot;
|
||||||
@@ -143,7 +143,7 @@ const initSecretVersioningHelper = async () => {
|
|||||||
|
|
||||||
if (unversionedSecrets.length > 0) {
|
if (unversionedSecrets.length > 0) {
|
||||||
await addSecretVersionsHelper({
|
await addSecretVersionsHelper({
|
||||||
secretVersions: unversionedSecrets.map((s, idx) => ({
|
secretVersions: unversionedSecrets.map((s, idx) => new SecretVersion({
|
||||||
...s,
|
...s,
|
||||||
secret: s._id,
|
secret: s._id,
|
||||||
version: s.version ? s.version : 1,
|
version: s.version ? s.version : 1,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
|
|
||||||
export interface ISecretVersion {
|
export interface ISecretVersion {
|
||||||
|
_id: Types.ObjectId;
|
||||||
secret: Types.ObjectId;
|
secret: Types.ObjectId;
|
||||||
version: number;
|
version: number;
|
||||||
workspace: Types.ObjectId; // new
|
workspace: Types.ObjectId; // new
|
||||||
@@ -18,7 +19,6 @@ export interface ISecretVersion {
|
|||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
tags?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||||
@@ -80,12 +80,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
secretValueTag: {
|
secretValueTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true
|
||||||
},
|
}
|
||||||
tags: {
|
|
||||||
ref: 'Tag',
|
|
||||||
type: [Schema.Types.ObjectId],
|
|
||||||
default: []
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class EESecretService {
|
|||||||
static async takeSecretSnapshot({
|
static async takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
return await takeSecretSnapshotHelper({ workspaceId });
|
return await takeSecretSnapshotHelper({ workspaceId });
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const createBot = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
let bot;
|
let bot;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
EELogService
|
EELogService
|
||||||
} from '../ee/services';
|
} from '../ee/services';
|
||||||
import {
|
import {
|
||||||
IAction
|
IAction,
|
||||||
|
SecretVersion
|
||||||
} from '../ee/models';
|
} from '../ee/models';
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
@@ -189,8 +190,7 @@ const v1PushSecrets = async ({
|
|||||||
secretKeyHash,
|
secretKeyHash,
|
||||||
}) => {
|
}) => {
|
||||||
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||||
return ({
|
return new SecretVersion({
|
||||||
_id: new Types.ObjectId(),
|
|
||||||
secret: _id,
|
secret: _id,
|
||||||
version: version ? version + 1 : 1,
|
version: version ? version + 1 : 1,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
@@ -261,8 +261,7 @@ const v1PushSecrets = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash
|
secretValueHash
|
||||||
}) => ({
|
}) => new SecretVersion({
|
||||||
_id: new Types.ObjectId(),
|
|
||||||
secret: _id,
|
secret: _id,
|
||||||
version,
|
version,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -284,7 +283,7 @@ const v1PushSecrets = async ({
|
|||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -475,12 +474,12 @@ const v2PushSecrets = async ({
|
|||||||
|
|
||||||
// (EE) add secret versions for new secrets
|
// (EE) add secret versions for new secrets
|
||||||
EESecretService.addSecretVersions({
|
EESecretService.addSecretVersions({
|
||||||
secretVersions: newSecrets.map((secretDocument) => {
|
secretVersions: newSecrets.map((secretDocument: ISecret) => {
|
||||||
return {
|
return new SecretVersion({
|
||||||
...secretDocument.toObject(),
|
...secretDocument,
|
||||||
secret: secretDocument._id,
|
secret: secretDocument._id,
|
||||||
isDeleted: false
|
isDeleted: false
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -495,7 +494,7 @@ const v2PushSecrets = async ({
|
|||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
})
|
})
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
CreateSecretParams,
|
||||||
|
GetSecretsParams,
|
||||||
|
GetSecretParams,
|
||||||
|
UpdateSecretParams,
|
||||||
|
DeleteSecretParams
|
||||||
|
} from '../interfaces/services/SecretService';
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
IUser,
|
IUser,
|
||||||
@@ -10,6 +17,10 @@ import {
|
|||||||
ISecret,
|
ISecret,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import {
|
||||||
|
IAction,
|
||||||
|
SecretVersion
|
||||||
|
} from '../ee/models';
|
||||||
import {
|
import {
|
||||||
validateMembership
|
validateMembership
|
||||||
} from '../helpers/membership';
|
} from '../helpers/membership';
|
||||||
@@ -27,17 +38,32 @@ import {
|
|||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
UnauthorizedRequestError,
|
UnauthorizedRequestError,
|
||||||
SecretNotFoundError
|
SecretNotFoundError,
|
||||||
|
SecretBlindIndexDataNotFoundError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
SECRET_SHARED,
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
|
import { decryptSymmetric } from '../utils/crypto';
|
||||||
|
import { getEncryptionKey } from '../config';
|
||||||
|
import {
|
||||||
|
EESecretService,
|
||||||
|
EELogService
|
||||||
|
} from '../ee/services';
|
||||||
|
import {
|
||||||
|
getAuthDataPayloadIdObj
|
||||||
|
} from '../utils/auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authenticated clients for secrets with id [secretId] based
|
* Validate authenticated clients for secrets with id [secretId] based
|
||||||
@@ -197,53 +223,13 @@ const validateClientForSecrets = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and return blind index for secret with
|
* Generate blind index for secret with name [secretName]
|
||||||
* name [name] part of workspace with id [workspaceId]
|
* for workspace with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.secretName - name of secret to generate blind index for
|
* @param {Object} obj.secretName - name of secret to generate blind index for
|
||||||
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
|
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
|
||||||
*/
|
*/
|
||||||
const createSecretBlindIndexHelper = async ({
|
const generateSecretBlindIndexHelper = async ({
|
||||||
secretName,
|
|
||||||
workspaceId
|
|
||||||
}: {
|
|
||||||
secretName: string;
|
|
||||||
workspaceId: Types.ObjectId;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
// check if workspace blind index data exists
|
|
||||||
// const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
|
||||||
// workspace: workspaceId
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!secretBlindIndexData) {
|
|
||||||
// // case: workspace blind index data has not been enabled
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: randomBytes should come from the decrypted secretBlindIndexData
|
|
||||||
const randomBytes = crypto.randomBytes(16);
|
|
||||||
|
|
||||||
const secretBlindIndex = (await argon2.hash(secretName, {
|
|
||||||
type: argon2.argon2id,
|
|
||||||
salt: randomBytes,
|
|
||||||
saltLength: 16, // default 16 bytes
|
|
||||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
|
||||||
hashLength: 32,
|
|
||||||
parallelism: 1,
|
|
||||||
raw: true
|
|
||||||
})).toString('base64');
|
|
||||||
|
|
||||||
return secretBlindIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the blind index for the secret with
|
|
||||||
* name [name] part of workspace with id [workspaceId]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.secretName - name of secret to generate blind index for
|
|
||||||
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
|
|
||||||
*/
|
|
||||||
const getSecretBlindIndexHelper = async ({
|
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
@@ -256,14 +242,505 @@ const getSecretBlindIndexHelper = async ({
|
|||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!secretBlindIndexData) {
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
// case: workspace blind index data has not been enabled
|
|
||||||
|
// decrypt workspace salt
|
||||||
|
const salt = decryptSymmetric({
|
||||||
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
|
iv: secretBlindIndexData.saltIV,
|
||||||
|
tag: secretBlindIndexData.saltTag,
|
||||||
|
key: getEncryptionKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate secret blind index
|
||||||
|
const secretBlindIndex = (await argon2.hash(secretName, {
|
||||||
|
type: argon2.argon2id,
|
||||||
|
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');
|
||||||
|
|
||||||
|
return secretBlindIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const createSecretHelper = async ({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}: CreateSecretParams) => {
|
||||||
|
// preliminary OK
|
||||||
|
// pending check for other types of clients
|
||||||
|
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const exists = await Secret.exists({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
type
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exists) throw BadRequestError({
|
||||||
|
message: 'Failed to create secret that already exists'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === SECRET_PERSONAL) {
|
||||||
|
// case: secret type is personal -> check if a corresponding shared secret
|
||||||
|
// with the same blind index [secretBlindIndex] exists
|
||||||
|
|
||||||
|
const exists = await Secret.exists({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exists) throw BadRequestError({
|
||||||
|
message: 'Failed to create personal secret override for no corresponding shared secret'
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: adapt to other client types
|
||||||
|
|
||||||
|
if (!(authData.authPayload instanceof User)) throw BadRequestError({
|
||||||
|
message: 'Failed to create personal secret override for no specified user'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create secret
|
||||||
|
const secret = await new Secret({
|
||||||
|
version: 1,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
...(type === SECRET_PERSONAL && (authData.authPayload instanceof User) ? {
|
||||||
|
user: authData.authPayload._id
|
||||||
|
} : {}),
|
||||||
|
secretBlindIndex,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
const secretVersion = new SecretVersion({
|
||||||
|
secret: secret._id,
|
||||||
|
version: secret.version,
|
||||||
|
workspace: secret.workspace,
|
||||||
|
type,
|
||||||
|
...(type === SECRET_PERSONAL && authData.authPayload instanceof User ? {
|
||||||
|
user: authData.authPayload._id
|
||||||
|
} : {}),
|
||||||
|
environment: secret.environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
});
|
||||||
|
|
||||||
|
// // (EE) add version for new secret
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: [secretVersion]
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
const action = await EELogService.createAction({
|
||||||
|
name: ACTION_ADD_SECRETS,
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: [secret._id]
|
||||||
|
});
|
||||||
|
|
||||||
|
action && await EELogService.createLog({
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
actions: [action],
|
||||||
|
channel: authData.authChannel,
|
||||||
|
ipAddress: authData.authIP
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const getSecretsHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
authData
|
||||||
|
}: GetSecretsParams) => {
|
||||||
|
// preliminary OK
|
||||||
|
// pending check for other types of clients
|
||||||
|
|
||||||
|
let secrets: ISecret[] = [];
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof User) {
|
||||||
|
// case: get personal secrets first
|
||||||
|
secrets = await Secret.find({
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: SECRET_PERSONAL,
|
||||||
|
user: authData.authPayload._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = secrets.concat(await Secret.find({
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: SECRET_SHARED,
|
||||||
|
secretBlindIndex: {
|
||||||
|
$nin: secrets.map((secret) => secret.secretBlindIndex)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const action = await EELogService.createAction({
|
||||||
|
name: ACTION_READ_SECRETS,
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: secrets.map((secret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
action && await EELogService.createLog({
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
actions: [action],
|
||||||
|
channel: authData.authChannel,
|
||||||
|
ipAddress: authData.authIP
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const getSecretHelper = async ({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData
|
||||||
|
}: GetSecretParams) => {
|
||||||
|
// preliminary OK
|
||||||
|
// pending check for other types of clients
|
||||||
|
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
let secret;
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof User) {
|
||||||
|
// case: find any personal secret matching criteria
|
||||||
|
secret = await Secret.findOne({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: type ?? SECRET_PERSONAL,
|
||||||
|
...(type === SECRET_PERSONAL ? {
|
||||||
|
user: authData.authPayload._id
|
||||||
|
} : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
// case: failed to find personal secret matching criteria
|
||||||
|
// -> find shared secret matching criteria
|
||||||
|
secret = await Secret.findOne({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secret) throw SecretNotFoundError();
|
||||||
|
|
||||||
|
const action = await EELogService.createAction({
|
||||||
|
name: ACTION_READ_SECRETS,
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: [secret._id]
|
||||||
|
});
|
||||||
|
|
||||||
|
action && await EELogService.createLog({
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
actions: [action],
|
||||||
|
channel: authData.authChannel,
|
||||||
|
ipAddress: authData.authIP
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const updateSecretHelper = async ({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}: UpdateSecretParams) => {
|
||||||
|
// preliminary OK
|
||||||
|
// pending check for other types of clients
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
let secret: ISecret | null = null;
|
||||||
|
|
||||||
|
if (type === SECRET_SHARED) {
|
||||||
|
secret = await Secret.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
secretBlindIndex,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
$inc: { version: 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (type === SECRET_PERSONAL && authData.authPayload instanceof User) {
|
||||||
|
secret = await Secret.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
secretBlindIndex,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
user: authData.authPayload._id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
$inc: { version: 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secret) throw SecretNotFoundError();
|
||||||
|
|
||||||
|
const secretVersion = new SecretVersion({
|
||||||
|
secret: secret._id,
|
||||||
|
version: secret.version,
|
||||||
|
workspace: secret.workspace,
|
||||||
|
type,
|
||||||
|
...(type === SECRET_PERSONAL && authData.authPayload instanceof User ? {
|
||||||
|
user: authData.authPayload._id
|
||||||
|
} : {}),
|
||||||
|
environment: secret.environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secret.secretKeyIV,
|
||||||
|
secretKeyTag: secret.secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
});
|
||||||
|
|
||||||
|
// // (EE) add version for new secret
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: [secretVersion]
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
const action = await EELogService.createAction({
|
||||||
|
name: ACTION_UPDATE_SECRETS,
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: [secret._id]
|
||||||
|
});
|
||||||
|
|
||||||
|
action && await EELogService.createLog({
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
actions: [action],
|
||||||
|
channel: authData.authChannel,
|
||||||
|
ipAddress: authData.authIP
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const deleteSecretHelper = async ({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData
|
||||||
|
}: DeleteSecretParams) => {
|
||||||
|
// preliminary OK
|
||||||
|
// pending check for other types of clients
|
||||||
|
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
let secrets: ISecret[] = [];
|
||||||
|
let secret: ISecret | null = null;
|
||||||
|
|
||||||
|
if (type === SECRET_SHARED) {
|
||||||
|
secrets = await Secret.find({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
|
||||||
|
secret = await Secret.findOneAndDelete({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
|
||||||
|
await Secret.deleteMany({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
} else if (type === SECRET_PERSONAL && authData.authPayload instanceof User) {
|
||||||
|
secret = await Secret.findOneAndDelete({
|
||||||
|
secretBlindIndex,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
user: authData.authPayload._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secret) {
|
||||||
|
secrets = [secret];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secret) throw SecretNotFoundError();
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: secrets.map((secret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
const action = await EELogService.createAction({
|
||||||
|
name: ACTION_DELETE_SECRETS,
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: secrets.map((secret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
action && await EELogService.createLog({
|
||||||
|
...getAuthDataPayloadIdObj(authData),
|
||||||
|
workspaceId,
|
||||||
|
actions: [action],
|
||||||
|
channel: authData.authChannel,
|
||||||
|
ipAddress: authData.authIP
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
secrets,
|
||||||
|
secret
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForSecret,
|
validateClientForSecret,
|
||||||
validateClientForSecrets,
|
validateClientForSecrets,
|
||||||
createSecretBlindIndexHelper,
|
generateSecretBlindIndexHelper,
|
||||||
getSecretBlindIndexHelper
|
createSecretHelper,
|
||||||
|
getSecretsHelper,
|
||||||
|
getSecretHelper,
|
||||||
|
updateSecretHelper,
|
||||||
|
deleteSecretHelper
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
|
SecretBlindIndexData
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { createBot } from '../helpers/bot';
|
import { createBot } from '../helpers/bot';
|
||||||
import { validateUserClientForWorkspace } from '../helpers/user';
|
import { validateUserClientForWorkspace } from '../helpers/user';
|
||||||
@@ -26,6 +28,8 @@ import {
|
|||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
import { getEncryptionKey } from '../config';
|
||||||
|
import { encryptSymmetric } from '../utils/crypto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authenticated clients for workspace with id [workspaceId] based
|
* Validate authenticated clients for workspace with id [workspaceId] based
|
||||||
@@ -42,7 +46,8 @@ const validateClientForWorkspace = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions,
|
||||||
|
requireBlindIndicesEnabled
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
@@ -52,6 +57,7 @@ const validateClientForWorkspace = async ({
|
|||||||
environment?: string;
|
environment?: string;
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
|
requireBlindIndicesEnabled: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
const workspace = await Workspace.findById(workspaceId);
|
||||||
@@ -60,6 +66,16 @@ const validateClientForWorkspace = async ({
|
|||||||
message: 'Failed to find workspace'
|
message: 'Failed to find workspace'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (requireBlindIndicesEnabled && !workspace.isBlindedIndicesEnabled) {
|
||||||
|
// case: blind indices are not enabled for secrets in this workspace
|
||||||
|
// (i.e. workspace was created before blind indices were introduced
|
||||||
|
// and no admin has enabled it)
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed workspace authorization due to blind indices not being enabled'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
const membership = await validateUserClientForWorkspace({
|
const membership = await validateUserClientForWorkspace({
|
||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
@@ -130,13 +146,36 @@ const createWorkspace = async ({
|
|||||||
// create workspace
|
// create workspace
|
||||||
workspace = await new Workspace({
|
workspace = await new Workspace({
|
||||||
name,
|
name,
|
||||||
organization: organizationId
|
organization: organizationId,
|
||||||
|
autoCapitalization: true,
|
||||||
|
isBlindedIndicesEnabled: true
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
const bot = await createBot({
|
// initialize bot for workspace
|
||||||
|
await createBot({
|
||||||
name: 'Infisical Bot',
|
name: 'Infisical Bot',
|
||||||
workspaceId: workspace._id.toString()
|
workspaceId: workspace._id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// initialize blind index salt for workspace
|
||||||
|
const salt = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSaltCiphertext,
|
||||||
|
iv: saltIV,
|
||||||
|
tag: saltTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: salt,
|
||||||
|
key: getEncryptionKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
await new SecretBlindIndexData({
|
||||||
|
workspace: workspace._id,
|
||||||
|
encryptedSaltCiphertext,
|
||||||
|
saltIV,
|
||||||
|
saltTag
|
||||||
|
}).save();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
12
backend/src/interfaces/middleware/index.ts
Normal file
12
backend/src/interfaces/middleware/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData
|
||||||
|
} from '../../models';
|
||||||
|
|
||||||
|
export interface AuthData {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
authChannel: string;
|
||||||
|
authIP: string;
|
||||||
|
}
|
||||||
49
backend/src/interfaces/services/SecretService/index.ts
Normal file
49
backend/src/interfaces/services/SecretService/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { AuthData } from '../../middleware';
|
||||||
|
|
||||||
|
export interface CreateSecretParams {
|
||||||
|
secretName: string;
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
authData: AuthData;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSecretsParams {
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
authData: AuthData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSecretParams {
|
||||||
|
secretName: string;
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
type?: 'shared' | 'personal';
|
||||||
|
authData: AuthData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSecretParams {
|
||||||
|
secretName: string;
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
type: 'shared' | 'personal',
|
||||||
|
authData: AuthData
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteSecretParams {
|
||||||
|
secretName: string;
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
authData: AuthData;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
import { getChannelFromUserAgent } from '../utils/posthog';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
@@ -88,7 +89,9 @@ const requireAuth = ({
|
|||||||
|
|
||||||
req.authData = {
|
req.authData = {
|
||||||
authMode,
|
authMode,
|
||||||
authPayload // User, ServiceAccount, ServiceTokenData
|
authPayload, // User, ServiceAccount, ServiceTokenData
|
||||||
|
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
|
authIP: req.ip
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ const requireWorkspaceAuth = ({
|
|||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
locationWorkspaceId,
|
locationWorkspaceId,
|
||||||
locationEnvironment = undefined,
|
locationEnvironment = undefined,
|
||||||
requiredPermissions = []
|
requiredPermissions = [],
|
||||||
|
requireBlindIndicesEnabled = false
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
locationWorkspaceId: req;
|
locationWorkspaceId: req;
|
||||||
locationEnvironment?: req | undefined;
|
locationEnvironment?: req | undefined;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
|
requireBlindIndicesEnabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
||||||
@@ -34,7 +36,8 @@ const requireWorkspaceAuth = ({
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions,
|
||||||
|
requireBlindIndicesEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
if (membership) {
|
if (membership) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Schema, model, Types, Document } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
@@ -59,7 +59,8 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretBlindIndex: {
|
secretBlindIndex: {
|
||||||
type: String
|
type: String,
|
||||||
|
select: false
|
||||||
},
|
},
|
||||||
secretKeyCiphertext: {
|
secretKeyCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { Schema, model, Types, Document } from 'mongoose';
|
|||||||
export interface ISecretBlindIndexData {
|
export interface ISecretBlindIndexData {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
encryptedSalt: string;
|
encryptedSaltCiphertext: string;
|
||||||
|
saltIV: string;
|
||||||
|
saltTag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
||||||
@@ -13,7 +15,15 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
|||||||
ref: 'Workspace',
|
ref: 'Workspace',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
encryptedSalt: {
|
encryptedSaltCiphertext: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
saltIV: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
saltTag: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import {
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_NOT_E2EE
|
|
||||||
} from '../variables';
|
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
encryptionMode: string;
|
|
||||||
organization: Types.ObjectId;
|
organization: Types.ObjectId;
|
||||||
environments: Array<{
|
environments: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}>;
|
}>;
|
||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
|
isBlindedIndicesEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceSchema = new Schema<IWorkspace>({
|
const workspaceSchema = new Schema<IWorkspace>({
|
||||||
@@ -22,19 +17,14 @@ const workspaceSchema = new Schema<IWorkspace>({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
encryptionMode: {
|
|
||||||
type: String,
|
|
||||||
default: 'e2ee',
|
|
||||||
enum: [
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_NOT_E2EE
|
|
||||||
]
|
|
||||||
},
|
|
||||||
autoCapitalization: {
|
autoCapitalization: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
isBlindedIndicesEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
organization: {
|
organization: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Organization',
|
ref: 'Organization',
|
||||||
@@ -70,4 +60,4 @@ const workspaceSchema = new Schema<IWorkspace>({
|
|||||||
|
|
||||||
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
|
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
|
||||||
|
|
||||||
export default Workspace;
|
export default Workspace;
|
||||||
@@ -1,18 +1,38 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
requireAuth, validateRequest
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
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 {
|
||||||
// note: future endpoints pending brainstorm + implementation
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
PERMISSION_WRITE_SECRETS,
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
PERMISSION_READ_SECRETS
|
||||||
|
} 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(),
|
||||||
query('tagSlugs'),
|
query('tagSlugs'),
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: 'query',
|
||||||
|
locationEnvironment: 'query',
|
||||||
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
|
requireBlindIndicesEnabled: true,
|
||||||
|
}),
|
||||||
secretsController.getSecrets
|
secretsController.getSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -20,6 +40,7 @@ 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('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(),
|
||||||
@@ -27,6 +48,16 @@ router.post(
|
|||||||
body('secretValueIV').exists().isString().trim(),
|
body('secretValueIV').exists().isString().trim(),
|
||||||
body('secretValueTag').exists().isString().trim(),
|
body('secretValueTag').exists().isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: 'body',
|
||||||
|
locationEnvironment: 'body',
|
||||||
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
|
requireBlindIndicesEnabled: true,
|
||||||
|
}),
|
||||||
secretsController.createSecret
|
secretsController.createSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,23 +66,61 @@ router.get(
|
|||||||
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]),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: 'query',
|
||||||
|
locationEnvironment: 'query',
|
||||||
|
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(),
|
||||||
query('workspaceId').exists().isString().trim(),
|
body('workspaceId').exists().isString().trim(),
|
||||||
query('environment').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,
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: 'body',
|
||||||
|
locationEnvironment: 'body',
|
||||||
|
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(),
|
||||||
query('workspaceId').exists().isString().trim(),
|
body('workspaceId').exists().isString().trim(),
|
||||||
|
body('environment').exists().isString().trim(),
|
||||||
|
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: 'body',
|
||||||
|
locationEnvironment: 'body',
|
||||||
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
|
requireBlindIndicesEnabled: true,
|
||||||
|
}),
|
||||||
secretsController.deleteSecretByName
|
secretsController.deleteSecretByName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,187 @@
|
|||||||
// WIP
|
// WIP
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ISecret
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
CreateSecretParams,
|
||||||
|
GetSecretsParams,
|
||||||
|
GetSecretParams,
|
||||||
|
UpdateSecretParams,
|
||||||
|
DeleteSecretParams
|
||||||
|
} from '../interfaces/services/SecretService';
|
||||||
import {
|
import {
|
||||||
createSecretBlindIndexHelper,
|
generateSecretBlindIndexHelper,
|
||||||
getSecretBlindIndexHelper
|
createSecretHelper,
|
||||||
|
getSecretsHelper,
|
||||||
|
getSecretHelper,
|
||||||
|
updateSecretHelper,
|
||||||
|
deleteSecretHelper
|
||||||
} from '../helpers/secrets';
|
} from '../helpers/secrets';
|
||||||
|
|
||||||
class SecretService {
|
class SecretService {
|
||||||
/**
|
/**
|
||||||
* Create and return blind index for secret with
|
* Create and return blind index for secret with
|
||||||
* name [name] part of workspace with id [workspaceId]
|
* 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 {Object} obj.secretName - name of secret to generate blind index for
|
||||||
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
|
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
|
||||||
*/
|
*/
|
||||||
static async createSecretBlindIndex({
|
static async generateSecretBlindIndex({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) {
|
}) {
|
||||||
return await createSecretBlindIndexHelper({
|
return await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId
|
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({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}: CreateSecretParams) {
|
||||||
|
return await createSecretHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the blind index for the secret with
|
* Get secrets for workspace with id [workspaceId] and environment [environment]
|
||||||
* name [name] 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 {Types.ObjectId} obj.workspaceId - id of workspace
|
||||||
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
|
* @param {String} obj.environment - environment in workspace
|
||||||
|
* @param {AuthData} obj.authData - authentication data on request
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async getSecretBlindIndex({
|
static async getSecrets({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
authData
|
||||||
|
}: GetSecretsParams) {
|
||||||
|
return await getSecretsHelper({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
authData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId
|
workspaceId,
|
||||||
}: {
|
environment,
|
||||||
secretName: string;
|
type,
|
||||||
workspaceId: Types.ObjectId;
|
authData
|
||||||
}) {
|
}: GetSecretParams) {
|
||||||
return await getSecretBlindIndexHelper({
|
return await getSecretHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
authData
|
||||||
|
}: UpdateSecretParams) {
|
||||||
|
return await updateSecretHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData
|
||||||
|
}: DeleteSecretParams) {
|
||||||
|
return await deleteSecretHelper({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ class Telemetry {
|
|||||||
serviceAccount?: IServiceAccount;
|
serviceAccount?: IServiceAccount;
|
||||||
serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated)
|
serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated)
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
|
// TODO: modify to accept authData instead
|
||||||
|
|
||||||
let distinctId = '';
|
let distinctId = '';
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|||||||
8
backend/src/types/express/index.d.ts
vendored
8
backend/src/types/express/index.d.ts
vendored
@@ -5,6 +5,9 @@ import {
|
|||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
ISecret
|
ISecret
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
|
import {
|
||||||
|
AuthData
|
||||||
|
} from '../../interfaces/middleware';
|
||||||
|
|
||||||
// TODO: fix (any) types
|
// TODO: fix (any) types
|
||||||
declare global {
|
declare global {
|
||||||
@@ -29,10 +32,7 @@ declare global {
|
|||||||
serviceTokenData: any;
|
serviceTokenData: any;
|
||||||
apiKeyData: any;
|
apiKeyData: any;
|
||||||
query?: any;
|
query?: any;
|
||||||
authData: {
|
authData: AuthData;
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
requestData: {
|
requestData: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
};
|
};
|
||||||
|
|||||||
26
backend/src/utils/auth.ts
Normal file
26
backend/src/utils/auth.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { AuthData } from '../interfaces/middleware';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
|
||||||
|
const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
||||||
|
if (authData.authPayload instanceof User) {
|
||||||
|
return { userId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceAccount) {
|
||||||
|
return { serviceAccountId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
return { serviceTokenDataId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAuthDataPayloadIdObj
|
||||||
|
}
|
||||||
@@ -153,6 +153,16 @@ export const SecretNotFoundError = (error?: Partial<RequestErrorContext>) => new
|
|||||||
stack: error?.stack
|
stack: error?.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//* ----->[SECRET BLIND INDEX DATA ERRORS]<-----
|
||||||
|
export const SecretBlindIndexDataNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
statusCode: error?.statusCode ?? 404,
|
||||||
|
type: error?.type ?? 'secret_blind_index_data_not_found_error',
|
||||||
|
message: error?.message ?? 'The requested secret was not found',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
|
||||||
//* ----->[SECRET SNAPSHOT ERRORS]<-----
|
//* ----->[SECRET SNAPSHOT ERRORS]<-----
|
||||||
export const SecretSnapshotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
export const SecretSnapshotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
|||||||
@@ -77,11 +77,6 @@ import {
|
|||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY
|
||||||
} from './authentication';
|
} from './authentication';
|
||||||
import {
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_NOT_E2EE
|
|
||||||
} from './workspace';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OWNER,
|
OWNER,
|
||||||
@@ -153,8 +148,5 @@ export {
|
|||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY,
|
AUTH_MODE_API_KEY
|
||||||
WORKSPACE_ENCRYPTION_MODE_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_NOT_E2EE
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
const WORKSPACE_ENCRYPTION_MODE_E2EE = 'e2ee';
|
|
||||||
const WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE = 'blind-indexed-e2ee';
|
|
||||||
const WORKSPACE_ENCRYPTION_MODE_NOT_E2EE = 'not-e2ee';
|
|
||||||
|
|
||||||
export {
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE,
|
|
||||||
WORKSPACE_ENCRYPTION_MODE_NOT_E2EE
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,93 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
useForm
|
||||||
|
} from 'react-hook-form';
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from '@app/components/v2';
|
||||||
|
|
||||||
|
// TODO: modify in accordance with what was discussed with
|
||||||
|
// Maidul
|
||||||
|
|
||||||
|
// Do with select for now them replace.
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ value: 'e2ee', label: 'E2EE' },
|
||||||
|
{ value: 'blind-indexed-e2ee', label: 'Blind Indexed E2EE' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = yup.object({
|
||||||
|
mode: yup.string().required().label('Project Mode')
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
export const ProjectEncryptionModeSection = () => {
|
export const ProjectEncryptionModeSection = () => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({ mode: 'blind-indexed-e2ee' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ mode }: FormData) => {
|
||||||
|
console.log('onFormSubmit');
|
||||||
|
console.log('mode: ', mode);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<form
|
||||||
Project encryption mode section
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
</div>
|
className="rounded-md bg-white/5 p-6"
|
||||||
|
>
|
||||||
|
<p className="mb-4 text-xl font-semibold">Encryption Mode</p>
|
||||||
|
<div className="mb-6 max-w-lg">
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
console.log('field: ', field);
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{items.map(item => (
|
||||||
|
<SelectItem value={item.value} key={`enc-mode-${item.value}`}>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
control={control}
|
||||||
|
name="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
color="mineshaft"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isDirty || isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user