diff --git a/backend/src/app.ts b/backend/src/app.ts index e49551a91..0fe39ae80 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,5 +1,6 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { patchRouterParam } = require('./utils/patchAsyncRoutes'); -import { patchRouterParam } from './utils/patchAsyncRoutes'; import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; @@ -10,6 +11,11 @@ dotenv.config(); import { PORT, NODE_ENV, SITE_URL } from './config'; import { apiLimiter } from './helpers/rateLimiter'; +import { + workspace as eeWorkspaceRouter, + secret as eeSecretRouter +} from './ee/routes'; + import { signup as signupRouter, auth as authRouter, @@ -29,12 +35,13 @@ import { integration as integrationRouter, integrationAuth as integrationAuthRouter } from './routes'; + import { getLogger } from './utils/logger'; import { RouteNotFoundError } from './utils/errors'; import { requestErrorHandler } from './middleware/requestErrorHandler'; -//* Patch Async route params to handle Promise Rejections -patchRouterParam() +// patch async route params to handle Promise Rejections +patchRouterParam(); export const app = express(); @@ -56,6 +63,10 @@ if (NODE_ENV === 'production') { app.use(helmet()); } +// /ee routers +app.use('/api/v1/secret', eeSecretRouter); +app.use('/api/v1/workspace', eeWorkspaceRouter); + // routers app.use('/api/v1/signup', signupRouter); app.use('/api/v1/auth', authRouter); diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index dfbc2111c..3fb475099 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -41,6 +41,7 @@ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true; +const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, @@ -83,5 +84,6 @@ export { STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, - TELEMETRY_ENABLED + TELEMETRY_ENABLED, + LICENSE_KEY }; diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/secretController.ts index bfd9aee1f..6672c4b49 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/secretController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Key } from '../models'; +import { Key, Secret } from '../models'; import { pushSecrets as push, pullSecrets as pull, @@ -169,9 +169,6 @@ export const pullSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecretsServiceToken = async (req: Request, res: Response) => { - // get (encrypted) secrets from workspace with id [workspaceId] - // service token route - let secrets; let key; try { @@ -225,4 +222,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { secrets: reformatPullSecrets({ secrets }), key }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/workspaceController.ts index a402834d7..6f3e4bd11 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/workspaceController.ts @@ -7,7 +7,7 @@ import { Integration, IntegrationAuth, IUser, - ServiceToken + ServiceToken, } from '../models'; import { createWorkspace as create, diff --git a/backend/src/ee/controllers/index.ts b/backend/src/ee/controllers/index.ts index e4fb89a8e..23880070d 100644 --- a/backend/src/ee/controllers/index.ts +++ b/backend/src/ee/controllers/index.ts @@ -1,5 +1,9 @@ import * as stripeController from './stripeController'; +import * as secretController from './secretController'; +import * as workspaceController from './workspaceController'; export { - stripeController + stripeController, + secretController, + workspaceController } \ No newline at end of file diff --git a/backend/src/ee/controllers/secretController.ts b/backend/src/ee/controllers/secretController.ts new file mode 100644 index 000000000..503a81c51 --- /dev/null +++ b/backend/src/ee/controllers/secretController.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretVersion } from '../models'; + +/** + * Return secret versions for secret with id [secretId] + * @param req + * @param res + */ + export const getSecretVersions = async (req: Request, res: Response) => { + let secretVersions; + try { + const { secretId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretVersions = await SecretVersion.find({ + secret: secretId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret versions' + }); + } + + return res.status(200).send({ + secretVersions + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/workspaceController.ts b/backend/src/ee/controllers/workspaceController.ts new file mode 100644 index 000000000..423e71793 --- /dev/null +++ b/backend/src/ee/controllers/workspaceController.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretSnapshot } from '../models'; + +/** + * Return secret snapshots for workspace with id [workspaceId] + * @param req + * @param res + */ + export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { + let secretSnapshots; + try { + const { workspaceId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretSnapshots = await SecretSnapshot.find({ + workspace: workspaceId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret snapshots' + }); + } + + return res.status(200).send({ + secretSnapshots + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/license.ts b/backend/src/ee/helpers/license.ts deleted file mode 100644 index 256bdc23a..000000000 --- a/backend/src/ee/helpers/license.ts +++ /dev/null @@ -1,21 +0,0 @@ - -/** - * @param {Object} obj - * @param {Object} obj.licenseKey - Infisical license key - */ -const checkLicenseKey = ({ - licenseKey -}: { - licenseKey: string -}) => { - try { - // TODO - - } catch (err) { - - } -} - -export { - checkLicenseKey -} \ No newline at end of file diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts new file mode 100644 index 000000000..a688a108f --- /dev/null +++ b/backend/src/ee/helpers/secret.ts @@ -0,0 +1,74 @@ +import * as Sentry from '@sentry/node'; +import { + Secret +} from '../../models'; +import { + SecretSnapshot, + SecretVersion, + ISecretVersion +} from '../models'; + +/** + * Save a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * secretsnapshots collection. + * @param {Object} obj + * @param {String} obj.workspaceId + */ + const takeSecretSnapshotHelper = async ({ + workspaceId +}: { + workspaceId: string; +}) => { + try { + const secrets = await Secret.find({ + workspace: workspaceId + }); + + const latestSecretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId + }).sort({ version: -1 }); + + if (!latestSecretSnapshot) { + // case: no snapshots exist for workspace -> create first snapshot + await new SecretSnapshot({ + workspace: workspaceId, + version: 1, + secrets + }).save(); + + return; + } + + // case: snapshots exist for workspace + await new SecretSnapshot({ + workspace: workspaceId, + version: latestSecretSnapshot.version + 1, + secrets + }).save(); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to take a secret snapshot'); + } +} + +const addSecretVersionsHelper = async ({ + secretVersions +}: { + secretVersions: ISecretVersion[] +}) => { + try { + await SecretVersion.insertMany(secretVersions); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to add secret versions'); + } +} + +export { + takeSecretSnapshotHelper, + addSecretVersionsHelper +} \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts new file mode 100644 index 000000000..35d41c19a --- /dev/null +++ b/backend/src/ee/models/index.ts @@ -0,0 +1,9 @@ +import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; +import SecretVersion, { ISecretVersion } from "./secretVersion"; + +export { + SecretSnapshot, + ISecretSnapshot, + SecretVersion, + ISecretVersion +} \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts new file mode 100644 index 000000000..69633a92e --- /dev/null +++ b/backend/src/ee/models/secretSnapshot.ts @@ -0,0 +1,109 @@ +import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../../variables'; + +export interface ISecretSnapshot { + workspace: Types.ObjectId; + version: number; + secrets: { + version: number; + workspace: Types.ObjectId; + type: string; + user: Types.ObjectId; + environment: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + }[] +} + +const secretSnapshotSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + version: { + type: Number, + required: true + }, + secrets: [{ + version: { + type: Number, + default: 1, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }] + }, + { + timestamps: true + } +); + +const SecretSnapshot = model('SecretSnapshot', secretSnapshotSchema); + +export default SecretSnapshot; \ No newline at end of file diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts new file mode 100644 index 000000000..a93a037f6 --- /dev/null +++ b/backend/src/ee/models/secretVersion.ts @@ -0,0 +1,75 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ISecretVersion { + _id?: Types.ObjectId; + secret: Types.ObjectId; + version: number; + isDeleted: boolean; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; +} + +const secretVersionSchema = new Schema( + { + secret: { // could be deleted + type: Schema.Types.ObjectId, + ref: 'Secret', + required: true + }, + version: { + type: Number, + default: 1, + required: true + }, + isDeleted: { + type: Boolean, + default: false, + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }, + { + timestamps: true + } +); + +const SecretVersion = model('SecretVersion', secretVersionSchema); + +export default SecretVersion; \ No newline at end of file diff --git a/backend/src/ee/routes/index.ts b/backend/src/ee/routes/index.ts new file mode 100644 index 000000000..960665f4a --- /dev/null +++ b/backend/src/ee/routes/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} \ No newline at end of file diff --git a/backend/src/ee/routes/secret.ts b/backend/src/ee/routes/secret.ts new file mode 100644 index 000000000..d8f1cb05b --- /dev/null +++ b/backend/src/ee/routes/secret.ts @@ -0,0 +1,26 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../middleware'; +import { body, query, param } from 'express-validator'; +import { secretController } from '../controllers'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; + +router.get( + '/:secretId/secret-versions', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('secretId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + secretController.getSecretVersions +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/workspace.ts b/backend/src/ee/routes/workspace.ts new file mode 100644 index 000000000..e27300eb3 --- /dev/null +++ b/backend/src/ee/routes/workspace.ts @@ -0,0 +1,27 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../middleware'; +import { param, query } from 'express-validator'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { workspaceController } from '../controllers'; + +router.get( + '/:workspaceId/secret-snapshots', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + workspaceController.getWorkspaceSecretSnapshots +); + + +export default router; \ No newline at end of file diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts new file mode 100644 index 000000000..f31482dde --- /dev/null +++ b/backend/src/ee/services/EELicenseService.ts @@ -0,0 +1,19 @@ +import { LICENSE_KEY } from '../../config'; + +/** + * Class to handle Enterprise Edition license actions + */ +class EELicenseService { + + private readonly _isLicenseValid: boolean; + + constructor(licenseKey: string) { + this._isLicenseValid = true; + } + + public get isLicenseValid(): boolean { + return this._isLicenseValid; + } +} + +export default new EELicenseService(LICENSE_KEY); \ No newline at end of file diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts new file mode 100644 index 000000000..643f763f1 --- /dev/null +++ b/backend/src/ee/services/EESecretService.ts @@ -0,0 +1,47 @@ +import { ISecretVersion } from '../models'; +import { + takeSecretSnapshotHelper, + addSecretVersionsHelper +} from '../helpers/secret'; +import EELicenseService from './EELicenseService'; + +/** + * Class to handle Enterprise Edition secret actions + */ +class EESecretService { + + /** + * Save a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * SecretSnapshot collection. + * Requires a valid license key [licenseKey] + * @param {Object} obj + * @param {String} obj.workspaceId + */ + static async takeSecretSnapshot({ + workspaceId + }: { + workspaceId: string; + }) { + if (!EELicenseService.isLicenseValid) return; + await takeSecretSnapshotHelper({ workspaceId }); + } + + /** + * Adds secret versions [secretVersions] to the SecretVersion collection. + * @param {Object} obj + * @param {SecretVersion} obj.secretVersions + */ + static async addSecretVersions({ + secretVersions + }: { + secretVersions: ISecretVersion[]; + }) { + if (!EELicenseService.isLicenseValid) return; + await addSecretVersionsHelper({ + secretVersions + }); + } +} + +export default EESecretService; \ No newline at end of file diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts new file mode 100644 index 000000000..3cec256bb --- /dev/null +++ b/backend/src/ee/services/index.ts @@ -0,0 +1,7 @@ +import EELicenseService from "./EELicenseService"; +import EESecretService from "./EESecretService"; + +export { + EELicenseService, + EESecretService +} \ No newline at end of file diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 042aba4fa..4b3585c40 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,10 +1,20 @@ import * as Sentry from '@sentry/node'; import { Secret, - ISecret + ISecret, } from '../models'; +import { + EESecretService +} from '../ee/services'; +import { + SecretVersion +} from '../ee/models'; +import { + takeSecretSnapshotHelper +} from '../ee/helpers/secret'; import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; +import { LICENSE_KEY } from '../config'; interface PushSecret { ciphertextKey: string; @@ -19,7 +29,7 @@ interface PushSecret { } interface Update { - [index: string]: string; + [index: string]: any; } type DecryptSecretType = 'text' | 'object' | 'expanded'; @@ -46,6 +56,7 @@ const pushSecrets = async ({ environment: string; secrets: PushSecret[]; }): Promise => { + // TODO: clean up function and fix up types try { // construct useful data structures const oldSecrets = await pullSecrets({ @@ -53,74 +64,124 @@ const pushSecrets = async ({ workspaceId, environment }); - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => { - return { ...accumulator, [s.secretKeyHash]: s }; - }, {}); - const newSecretsObj = secrets.reduce((accumulator, s) => { - return { ...accumulator, [s.hashKey]: s }; - }, {}); + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => + ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) + , {}); // handle deleting secrets - const toDelete = oldSecrets.filter( - (s: ISecret) => !(s.secretKeyHash in newSecretsObj) - ); + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) + ) + .map((s) => s._id); if (toDelete.length > 0) { await Secret.deleteMany({ - _id: { $in: toDelete.map((s) => s._id) } + _id: { $in: toDelete } + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true }); } - - // handle modifying secrets where type or value changed - const operations = secrets + + const toUpdate = oldSecrets .filter((s) => { - if (s.hashKey in oldSecretsObj) { - if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) { // case: filter secrets where value changed return true; } - if (s.type !== oldSecretsObj[s.hashKey].type) { - // case: filter secrets where type changed + if (!s.version) { + // case: filter (legacy) secrets that were not versioned return true; } } - + return false; - }) + }); + + const operations = toUpdate .map((s) => { + const { + ciphertextValue, + ivValue, + tagValue, + hashValue + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + const update: Update = { - type: s.type, - secretValueCiphertext: s.ciphertextValue, - secretValueIV: s.ivValue, - secretValueTag: s.tagValue, - secretValueHash: s.hashValue - }; + secretValueCiphertext: ciphertextValue, + secretValueIV: ivValue, + secretValueTag: tagValue, + secretValueHash: hashValue + } + + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update['$inc'] = { + version: 1 + } + } if (s.type === SECRET_PERSONAL) { - // attach user assocaited with the personal secret + // attach user associated with the personal secret update['user'] = userId; } return { updateOne: { filter: { - workspace: workspaceId, - _id: oldSecretsObj[s.hashKey]._id + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id }, update } }; }); - const a = await Secret.bulkWrite(operations as any); + await Secret.bulkWrite(operations as any); + + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map(({ + _id, + version, + type, + secretKeyHash, + }) => { + const newSecret = newSecretsObj[`${type}-${secretKeyHash}`]; + return ({ + secret: _id, + version: version ? version + 1 : 1, + isDeleted: false, + secretKeyCiphertext: newSecret.ciphertextKey, + secretKeyIV: newSecret.ivKey, + secretKeyTag: newSecret.tagKey, + secretKeyHash: newSecret.hashKey, + secretValueCiphertext: newSecret.ciphertextValue, + secretValueIV: newSecret.ivValue, + secretValueTag: newSecret.tagValue, + secretValueHash: newSecret.hashValue + }) + }) + }); // handle adding new secrets - const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); + const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj)); if (toAdd.length > 0) { // add secrets - await Secret.insertMany( + const newSecrets = await Secret.insertMany( toAdd.map((s, idx) => { - let obj: any = { + const obj: any = { + version: 1, workspace: workspaceId, type: toAdd[idx].type, environment, @@ -141,7 +202,39 @@ const pushSecrets = async ({ return obj; }) ); + + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + }); } + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }) } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -295,6 +388,8 @@ const decryptSecrets = ({ return content; }; + + export { pushSecrets, pullSecrets, diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index b83ef728d..d34139ecb 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -10,6 +10,7 @@ import { export interface ISecret { _id: Types.ObjectId; + version: number; workspace: Types.ObjectId; type: string; user: Types.ObjectId; @@ -26,6 +27,10 @@ export interface ISecret { const secretSchema = new Schema( { + version: { + type: Number, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', diff --git a/backend/src/routes/secret.ts b/backend/src/routes/secret.ts index 98b3009de..26224fd87 100644 --- a/backend/src/routes/secret.ts +++ b/backend/src/routes/secret.ts @@ -7,8 +7,8 @@ import { validateRequest } from '../middleware'; import { body, query, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { secretController } from '../controllers'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; router.post( '/:workspaceId', diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/workspace.ts index 1d20c102a..acd2aaf8b 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/workspace.ts @@ -1,6 +1,6 @@ import express from 'express'; const router = express.Router(); -import { body, param } from 'express-validator'; +import { body, param, query } from 'express-validator'; import { requireAuth, requireWorkspaceAuth, diff --git a/backend/src/utils/patchAsyncRoutes.js b/backend/src/utils/patchAsyncRoutes.js index 6f6d2367f..24fe007f9 100644 --- a/backend/src/utils/patchAsyncRoutes.js +++ b/backend/src/utils/patchAsyncRoutes.js @@ -45,7 +45,7 @@ function wrap(fn) { return copyFnProps(fn, newFn); } -export function patchRouterParam() { +function patchRouterParam() { const originalParam = Router.prototype.constructor.param; Router.prototype.constructor.param = function param(name, fn) { fn = wrap(fn); @@ -62,4 +62,8 @@ Object.defineProperty(Layer.prototype, 'handle', { fn = wrap(fn); this.__handle = fn; }, -}); \ No newline at end of file +}); + +module.exports = { + patchRouterParam +}; diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 0b9fd5e71..f598336b6 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -28,6 +28,7 @@ Configuring Infisical requires setting some environment variables. There is a fi | `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` | | `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` | | `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` | +| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` | | `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` | | `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` | | `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` |