diff --git a/backend/src/app.ts b/backend/src/app.ts index e49551a912..2178e91e8b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -10,6 +10,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,6 +34,7 @@ import { integration as integrationRouter, integrationAuth as integrationAuthRouter } from './routes'; + import { getLogger } from './utils/logger'; import { RouteNotFoundError } from './utils/errors'; import { requestErrorHandler } from './middleware/requestErrorHandler'; @@ -56,6 +62,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 dfbc2111cb..3fb4750992 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 73f7d06061..6672c4b49f 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, Secret, SecretVersion } from '../models'; +import { Key, Secret } from '../models'; import { pushSecrets as push, pullSecrets as pull, @@ -222,36 +222,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { secrets: reformatPullSecrets({ secrets }), key }); -}; - -/** - * 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 +}; \ No newline at end of file diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/workspaceController.ts index 7f901d0b2c..6f3e4bd115 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/workspaceController.ts @@ -8,7 +8,6 @@ import { IntegrationAuth, IUser, ServiceToken, - SecretSnapshot } from '../models'; import { createWorkspace as create, @@ -335,36 +334,4 @@ export const getWorkspaceServiceTokens = async ( return res.status(200).send({ serviceTokens }); -} - -/** - * 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/controllers/index.ts b/backend/src/ee/controllers/index.ts index e4fb89a8e3..23880070d4 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 0000000000..503a81c514 --- /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 0000000000..423e717936 --- /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 256bdc23a9..0000000000 --- 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 0000000000..57fc178eaf --- /dev/null +++ b/backend/src/ee/helpers/secret.ts @@ -0,0 +1,57 @@ +import * as Sentry from '@sentry/node'; +import { + Secret +} from '../../models'; +import { + SecretSnapshot +} 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'); + } +} + +export { + takeSecretSnapshotHelper +} \ 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 0000000000..930c01c4fe --- /dev/null +++ b/backend/src/ee/models/index.ts @@ -0,0 +1,7 @@ +import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; +import SecretVersion, { ISecretVersion } from "./secretVersion"; + +export { + SecretSnapshot, + SecretVersion +} \ No newline at end of file diff --git a/backend/src/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts similarity index 99% rename from backend/src/models/secretSnapshot.ts rename to backend/src/ee/models/secretSnapshot.ts index 3761153088..69633a92e4 100644 --- a/backend/src/models/secretSnapshot.ts +++ b/backend/src/ee/models/secretSnapshot.ts @@ -6,7 +6,7 @@ import { ENV_TESTING, ENV_STAGING, ENV_PROD -} from '../variables'; +} from '../../variables'; export interface ISecretSnapshot { workspace: Types.ObjectId; diff --git a/backend/src/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts similarity index 100% rename from backend/src/models/secretVersion.ts rename to backend/src/ee/models/secretVersion.ts diff --git a/backend/src/ee/routes/index.ts b/backend/src/ee/routes/index.ts new file mode 100644 index 0000000000..960665f4a9 --- /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 0000000000..d8f1cb05b1 --- /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 0000000000..e27300eb35 --- /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 0000000000..2c0228ef50 --- /dev/null +++ b/backend/src/ee/services/EELicenseService.ts @@ -0,0 +1,22 @@ +/** + * Class to handle Enterprise Edition license actions + */ +class EELicenseService { + /** + * Check if license key [licenseKey] corresponds to a + * valid Infisical Enterprise Edition license. + * @param {Object} obj + * @param {Object} obj.licenseKey + * @returns {Boolean} + */ + static async checkLicense({ + licenseKey + }: { + licenseKey: string; + }) { + // TODO + return true; + } +} + +export default EELicenseService; \ 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 0000000000..dca8bdfe48 --- /dev/null +++ b/backend/src/ee/services/EESecretService.ts @@ -0,0 +1,29 @@ +import { takeSecretSnapshotHelper } 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({ + licenseKey, + workspaceId + }: { + licenseKey: string; + workspaceId: string; + }) { + EELicenseService.checkLicense({ licenseKey }); + await takeSecretSnapshotHelper({ workspaceId }); + } +} + +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 0000000000..3cec256bb7 --- /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 5c8edddf79..630bff4587 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -2,13 +2,19 @@ import * as Sentry from '@sentry/node'; import { Secret, ISecret, - SecretVersion, - ISecretVersion, - SecretSnapshot, - ISecretSnapshot } 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; @@ -206,9 +212,11 @@ const pushSecrets = async ({ ); } - await takeSecretSnapshotHelper({ + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + licenseKey: LICENSE_KEY, workspaceId - }); + }) } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -362,56 +370,11 @@ const decryptSecrets = ({ return content; }; -/** - * Saves 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'); - } -} export { pushSecrets, pullSecrets, reformatPullSecrets, - decryptSecrets, - takeSecretSnapshotHelper + decryptSecrets }; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index daab77b2a8..78c38060b9 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -9,8 +9,6 @@ import Membership, { IMembership } from './membership'; import MembershipOrg, { IMembershipOrg } from './membershipOrg'; import Organization, { IOrganization } from './organization'; import Secret, { ISecret } from './secret'; -import SecretVersion, { ISecretVersion } from './secretVersion'; -import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot'; import ServiceToken, { IServiceToken } from './serviceToken'; import Token, { IToken } from './token'; import User, { IUser } from './user'; @@ -40,10 +38,6 @@ export { IOrganization, Secret, ISecret, - SecretVersion, - ISecretVersion, - SecretSnapshot, - ISecretSnapshot, ServiceToken, IServiceToken, Token, diff --git a/backend/src/routes/secret.ts b/backend/src/routes/secret.ts index 073384129e..26224fd87f 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', @@ -50,18 +50,4 @@ router.get( secretController.pullSecretsServiceToken ); -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; diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/workspace.ts index c5ffd739ed..acd2aaf8bf 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/workspace.ts @@ -130,18 +130,4 @@ router.get( workspaceController.getWorkspaceServiceTokens ); -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; diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 0b9fd5e71a..f598336b6a 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` |