From 8341faddc513f9c915b626b21af456884ddb4bf3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 22 May 2023 15:43:33 +0300 Subject: [PATCH 01/17] Add support for pulling plan details from license server with LICENSE_KEY, LICENSE_SERVER_KEY --- backend/package-lock.json | 1 + backend/package.json | 1 + backend/src/config/index.ts | 7 ++ backend/src/config/request.ts | 114 +++++++++++++++++- backend/src/config/storage.ts | 30 +++++ .../v1/integrationAuthController.ts | 8 +- .../src/controllers/v2/signupController.ts | 4 +- backend/src/ee/controllers/v1/index.ts | 2 + .../controllers/v1/organizationsController.ts | 15 +++ backend/src/ee/helpers/organizations.ts | 39 ++++++ backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/routes/v1/organizations.ts | 28 +++++ backend/src/ee/routes/v1/secretSnapshot.ts | 2 +- backend/src/ee/services/EELicenseService.ts | 95 ++++++++++++++- backend/src/helpers/workspace.ts | 4 +- backend/src/index.ts | 11 +- backend/src/integrations/apps.ts | 26 ++-- backend/src/integrations/exchange.ts | 18 +-- backend/src/integrations/refresh.ts | 8 +- backend/src/integrations/sync.ts | 77 ++++++------ backend/src/integrations/teams.ts | 4 +- .../src/middleware/requireWorkspaceAuth.ts | 6 +- backend/src/models/integration.ts | 6 + docker-compose.dev.yml | 2 + 24 files changed, 422 insertions(+), 88 deletions(-) create mode 100644 backend/src/config/storage.ts create mode 100644 backend/src/ee/controllers/v1/organizationsController.ts create mode 100644 backend/src/ee/helpers/organizations.ts create mode 100644 backend/src/ee/routes/v1/organizations.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 8498f3c0d..b82659229 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -40,6 +40,7 @@ "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^6.10.5", + "node-cache": "^5.1.2", "nodemailer": "^6.8.0", "posthog-node": "^2.6.0", "query-string": "^7.1.3", diff --git a/backend/package.json b/backend/package.json index 6ab43aa8f..efa468d44 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,6 +31,7 @@ "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^6.10.5", + "node-cache": "^5.1.2", "nodemailer": "^6.8.0", "posthog-node": "^2.6.0", "query-string": "^7.1.3", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 5dce2bc09..7d46580a1 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -45,12 +45,19 @@ export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAM export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue; export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue; export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical'; + +export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue; +export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue; +export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com'; + +// TODO: deprecate from here export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue; export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue; export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue; export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue; export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue; export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue; + export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true; export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue; export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true diff --git a/backend/src/config/request.ts b/backend/src/config/request.ts index 2a9a8279a..e13469657 100644 --- a/backend/src/config/request.ts +++ b/backend/src/config/request.ts @@ -1,10 +1,24 @@ import axios from 'axios'; import axiosRetry from 'axios-retry'; +import { + getLicenseServerKeyAuthToken, + setLicenseServerKeyAuthToken, + getLicenseKeyAuthToken, + setLicenseKeyAuthToken +} from './storage'; +import { + getLicenseKey, + getLicenseServerKey, + getLicenseServerUrl +} from './index'; -const axiosInstance = axios.create(); +// should have JWT to interact with the license server +export const licenseServerKeyRequest = axios.create(); +export const licenseKeyRequest = axios.create(); +export const standardRequest = axios.create(); // add retry functionality to the axios instance -axiosRetry(axiosInstance, { +axiosRetry(standardRequest, { retries: 3, retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries retryCondition: (error) => { @@ -13,4 +27,98 @@ axiosRetry(axiosInstance, { }, }); -export default axiosInstance; \ No newline at end of file +export const refreshLicenseServerKeyToken = async () => { + const licenseServerKey = await getLicenseServerKey(); + const licenseServerUrl = await getLicenseServerUrl(); + + const { data: { token } } = await standardRequest.post( + `${licenseServerUrl}/api/auth/v1/license-server-login`, {}, + { + headers: { + 'X-API-KEY': licenseServerKey + } + } + ); + + setLicenseServerKeyAuthToken(token); + + return token; +} + +export const refreshLicenseKeyToken = async () => { + const licenseKey = await getLicenseKey(); + const licenseServerUrl = await getLicenseServerUrl(); + + const { data: { token } } = await standardRequest.post( + `${licenseServerUrl}/api/auth/v1/license-login`, {}, + { + headers: { + 'X-API-KEY': licenseKey + } + } + ); + + setLicenseKeyAuthToken(token); + + return token; +} + +licenseServerKeyRequest.interceptors.request.use((config) => { + const token = getLicenseServerKeyAuthToken(); + + if (token && config.headers) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}, (err) => { + return Promise.reject(err); +}); + +licenseServerKeyRequest.interceptors.response.use((response) => { + return response +}, async function (err) { + const originalRequest = err.config; + + if (err.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + // refresh + const token = await refreshLicenseServerKeyToken(); + + axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; + return licenseServerKeyRequest(originalRequest); + } + + return Promise.reject(err); +}); + +licenseKeyRequest.interceptors.request.use((config) => { + const token = getLicenseKeyAuthToken(); + + if (token && config.headers) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}, (err) => { + return Promise.reject(err); +}); + +licenseKeyRequest.interceptors.response.use((response) => { + return response +}, async function (err) { + const originalRequest = err.config; + + if (err.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + // refresh + const token = await refreshLicenseKeyToken(); + + axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; + return licenseKeyRequest(originalRequest); + } + + return Promise.reject(err); +}); \ No newline at end of file diff --git a/backend/src/config/storage.ts b/backend/src/config/storage.ts new file mode 100644 index 000000000..5638561ac --- /dev/null +++ b/backend/src/config/storage.ts @@ -0,0 +1,30 @@ +const MemoryLicenseServerKeyTokenStorage = () => { + let authToken: string; + + return { + setToken: (token: string) => { + authToken = token; + }, + getToken: () => authToken + }; +}; + +const MemoryLicenseKeyTokenStorage = () => { + let authToken: string; + + return { + setToken: (token: string) => { + authToken = token; + }, + getToken: () => authToken + }; +}; + +const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage(); +const licenseTokenStorage = MemoryLicenseKeyTokenStorage(); + +export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken; +export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken; + +export const getLicenseKeyAuthToken = licenseTokenStorage.getToken; +export const setLicenseKeyAuthToken = licenseTokenStorage.setToken; \ No newline at end of file diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index b21a0cd42..0d5947a4f 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -16,7 +16,7 @@ import { INTEGRATION_VERCEL_API_URL, INTEGRATION_RAILWAY_API_URL } from '../../variables'; -import request from '../../config/request'; +import { standardRequest } from '../../config/request'; /*** * Return integration authorization with id [integrationAuthId] @@ -229,7 +229,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon let branches: string[] = []; if (appId && appId !== '') { - const { data }: { data: VercelBranch[] } = await request.get( + const { data }: { data: VercelBranch[] } = await standardRequest.get( `${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`, { params, @@ -292,7 +292,7 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R projectId: appId } - const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, { + const { data: { data: { environments: { edges } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { query, variables, }, { @@ -372,7 +372,7 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo id: appId } - const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, { + const { data: { data: { project: { services: { edges } } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { query, variables }, { diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index 7cfb3e454..28d40403c 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -7,7 +7,7 @@ import { } from '../../helpers/signup'; import { issueAuthTokens } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; -import request from '../../config/request'; +import { standardRequest } from '../../config/request'; import { getLoopsApiKey, getHttpsEnabled } from '../../config'; /** @@ -109,7 +109,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // sending a welcome email to new users if (await getLoopsApiKey()) { - await request.post("https://app.loops.so/api/v1/events/send", { + await standardRequest.post("https://app.loops.so/api/v1/events/send", { "email": email, "eventName": "Sign Up", "firstName": firstName, diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index aaa697286..b9618495f 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -1,6 +1,7 @@ import * as stripeController from './stripeController'; import * as secretController from './secretController'; import * as secretSnapshotController from './secretSnapshotController'; +import * as organizationsController from './organizationsController'; import * as workspaceController from './workspaceController'; import * as actionController from './actionController'; import * as membershipController from './membershipController'; @@ -9,6 +10,7 @@ export { stripeController, secretController, secretSnapshotController, + organizationsController, workspaceController, actionController, membershipController diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts new file mode 100644 index 000000000..1f9cf1080 --- /dev/null +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -0,0 +1,15 @@ +import { Types } from 'mongoose'; +import { Request, Response } from 'express'; +import { getOrganizationPlanHelper } from '../../helpers/organizations'; + +export const getOrganizationPlan = async (req: Request, res: Response) => { + const { organizationId } = req.params; + + const plan = await getOrganizationPlanHelper({ + organizationId: new Types.ObjectId(organizationId) + }); + + return res.status(200).send({ + plan + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/organizations.ts b/backend/src/ee/helpers/organizations.ts new file mode 100644 index 000000000..80b1048be --- /dev/null +++ b/backend/src/ee/helpers/organizations.ts @@ -0,0 +1,39 @@ +import { Types } from 'mongoose'; +import * as Sentry from '@sentry/node'; +import { Organization } from '../../models'; +import { EELicenseService } from '../services'; +import { getLicenseServerUrl } from '../../config'; +import { licenseServerKeyRequest } from '../../config/request'; +import { OrganizationNotFoundError } from '../../utils/errors'; + +export const getOrganizationPlanHelper = async ({ + organizationId +}: { + organizationId: Types.ObjectId; +}) => { + try { + if (EELicenseService.instanceType === 'cloud') { + // instance of Infisical is a cloud instance + + const organization = await Organization.findById(organizationId); + if (!organization) throw OrganizationNotFoundError(); + + const cachedPlan = EELicenseService.localFeatureSet.get(organizationId.toString()); + if (cachedPlan) return cachedPlan; + + const { data: { currentPlan } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` + ); + + // cache fetched plan for organization + EELicenseService.localFeatureSet.set(organizationId.toString(), currentPlan); + return currentPlan; + } + + return EELicenseService.globalFeatureSet; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return EELicenseService.globalFeatureSet; + } +} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 612715111..9b4d30b1b 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,11 +1,13 @@ import secret from './secret'; import secretSnapshot from './secretSnapshot'; +import organizations from './organizations'; import workspace from './workspace'; import action from './action'; export { secret, secretSnapshot, + organizations, workspace, action } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts new file mode 100644 index 000000000..3f208b3c2 --- /dev/null +++ b/backend/src/ee/routes/v1/organizations.ts @@ -0,0 +1,28 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireOrganizationAuth, + validateRequest +} from '../../../middleware'; +import { param } from 'express-validator'; +import { organizationsController } from '../../controllers/v1'; +import { + OWNER, ADMIN, MEMBER, ACCEPTED +} from '../../../variables'; + +router.get( + '/:organizationId/plan', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + validateRequest, + organizationsController.getOrganizationPlan +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index d10da4456..80aa7d1ee 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -7,7 +7,7 @@ import { requireAuth, validateRequest } from '../../../middleware'; -import { param, body } from 'express-validator'; +import { param } from 'express-validator'; import { ADMIN, MEMBER } from '../../../variables'; import { secretSnapshotController } from '../../controllers/v1'; diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 4bd811340..02cab7e5d 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -1,12 +1,99 @@ +import NodeCache from 'node-cache'; +import * as Sentry from '@sentry/node'; +import { + getLicenseKey, + getLicenseServerKey, + getLicenseServerUrl +} from '../../config'; +import { + licenseKeyRequest, + refreshLicenseServerKeyToken, + refreshLicenseKeyToken +} from '../../config/request'; + +interface FeatureSet { + _id: string | null; + slug: 'starter' | 'team' | 'pro' | 'enterprise' | null; + tier: number | null; + projectLimit: number | null; + memberLimit: number | null; + secretVersioning: boolean; + pitRecovery: boolean; + rbac: boolean; + customRateLimits: boolean; + customAlerts: boolean; + auditLogs: boolean; +} + /** - * Class to handle Enterprise Edition license actions + * Class to handle license/plan configurations: + * - Infisical Cloud: Fetch and cache customer plans in [localFeatureSet] + * - Self-hosted regular: Use default global feature set + * - Self-hosted enterprise: Fetch and update global feature set */ class EELicenseService { - private readonly _isLicenseValid: boolean; + private readonly _isLicenseValid: boolean; // TODO: deprecate + + public instanceType: 'self-hosted' | 'enterprise-self-hosted' | 'cloud' = 'self-hosted'; + + public globalFeatureSet: FeatureSet = { + _id: null, + slug: null, + tier: null, + projectLimit: null, + memberLimit: null, + secretVersioning: true, + pitRecovery: true, + rbac: true, + customRateLimits: true, + customAlerts: true, + auditLogs: false + } + + public localFeatureSet: NodeCache; - constructor(licenseKey: string) { + constructor() { this._isLicenseValid = true; + this.localFeatureSet = new NodeCache({ + stdTTL: 300 + }); + } + + public async initGlobalFeatureSet() { + const licenseServerKey = await getLicenseServerKey(); + const licenseKey = await getLicenseKey(); + + try { + if (licenseServerKey) { + // license server key is present -> validate it + const token = await refreshLicenseServerKeyToken() + + if (token) { + this.instanceType = 'cloud'; + } + + return; + } + + if (licenseKey) { + // license key is present -> validate it + const token = await refreshLicenseKeyToken(); + + if (token) { + const { data: { currentPlan } } = await licenseKeyRequest.get( + `${await getLicenseServerUrl()}/api/license/v1/plan` + ); + + this.globalFeatureSet = currentPlan; + this.instanceType = 'enterprise-self-hosted'; + } + } + } catch (err) { + // case: self-hosted free + Sentry.setUser(null); + Sentry.captureException(err); + } } public get isLicenseValid(): boolean { @@ -14,4 +101,4 @@ class EELicenseService { } } -export default new EELicenseService('N/A'); \ No newline at end of file +export default new EELicenseService(); \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 5047a1967..2c6a50e21 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -89,7 +89,7 @@ const validateClientForWorkspace = async ({ requiredPermissions }); - return ({ membership }); + return ({ membership, workspace }); } if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { @@ -123,7 +123,7 @@ const validateClientForWorkspace = async ({ requiredPermissions }); - return ({ membership }); + return ({ membership, workspace }); } throw UnauthorizedRequestError({ diff --git a/backend/src/index.ts b/backend/src/index.ts index 7270470ce..c4ba7c846 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,3 @@ -import mongoose from 'mongoose'; import dotenv from 'dotenv'; dotenv.config(); import express from 'express'; @@ -6,6 +5,7 @@ import helmet from 'helmet'; import cors from 'cors'; import * as Sentry from '@sentry/node'; import { DatabaseService } from './services'; +import { EELicenseService } from './ee/services'; import { setUpHealthEndpoint } from './services/health'; import { initSmtp } from './services/smtp'; import { TelemetryService } from './services'; @@ -25,7 +25,8 @@ import { workspace as eeWorkspaceRouter, secret as eeSecretRouter, secretSnapshot as eeSecretSnapshotRouter, - action as eeActionRouter + action as eeActionRouter, + organizations as eeOrganizationsRouter } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -74,14 +75,15 @@ import { getNodeEnv, getPort, getSentryDSN, - getSiteURL, - getSmtpHost + getSiteURL } from './config'; const main = async () => { TelemetryService.logTelemetryMessage(); setTransporter(await initSmtp()); + await EELicenseService.initGlobalFeatureSet(); + await DatabaseService.initDatabase(await getMongoURL()); if ((await getNodeEnv()) !== 'test') { Sentry.init({ @@ -119,6 +121,7 @@ const main = async () => { app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/action', eeActionRouter); + app.use('/api/v1/organizations', eeOrganizationsRouter); // v1 routes (default) app.use('/api/v1/signup', v1SignupRouter); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index a748f199f..fa0020d61 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,6 +1,6 @@ import { Octokit } from "@octokit/rest"; import { IIntegrationAuth } from "../models"; -import request from "../config/request"; +import { standardRequest } from "../config/request"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -134,7 +134,7 @@ const getApps = async ({ */ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { const res = ( - await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { + await standardRequest.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}`, @@ -164,7 +164,7 @@ const getAppsVercel = async ({ accessToken: string; }) => { const res = ( - await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { + await standardRequest.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json", @@ -208,7 +208,7 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { filter: 'all' }); - const { data } = await request.get( + const { data } = await standardRequest.get( `${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { params, @@ -310,7 +310,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { */ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { const res = ( - await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { + await standardRequest.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", @@ -358,7 +358,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { projects: { edges }, }, }, - } = await request.post( + } = await standardRequest.post( INTEGRATION_RAILWAY_API_URL, { query, @@ -402,7 +402,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { `; const res = ( - await request.post( + await standardRequest.post( INTEGRATION_FLYIO_API_URL, { query, @@ -436,7 +436,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { */ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { const res = ( - await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, { + await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json", @@ -455,7 +455,7 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { const res = ( - await request.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, { + await standardRequest.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, { headers: { Authorization: `token ${accessToken}`, "Accept-Encoding": "application/json", @@ -502,7 +502,7 @@ const getAppsGitlab = async ({ per_page: String(perPage), }); - const { data } = await request.get( + const { data } = await standardRequest.get( `${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`, { params, @@ -530,7 +530,7 @@ const getAppsGitlab = async ({ // case: fetch projects for individual in GitLab const { id } = ( - await request.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, { + await standardRequest.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json", @@ -544,7 +544,7 @@ const getAppsGitlab = async ({ per_page: String(perPage), }); - const { data } = await request.get( + const { data } = await standardRequest.get( `${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`, { params, @@ -581,7 +581,7 @@ const getAppsGitlab = async ({ * @returns {String} apps.name - name of Supabase app */ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { - const { data } = await request.get( + const { data } = await standardRequest.get( `${INTEGRATION_SUPABASE_API_URL}/v1/projects`, { headers: { diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index f7bb0222b..a4d5f5b06 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -1,4 +1,4 @@ -import request from "../config/request"; +import { standardRequest } from "../config/request"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -142,7 +142,7 @@ const exchangeCodeAzure = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); const res: ExchangeCodeAzureResponse = ( - await request.post( + await standardRequest.post( INTEGRATION_AZURE_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", @@ -178,7 +178,7 @@ const exchangeCodeHeroku = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); const res: ExchangeCodeHerokuResponse = ( - await request.post( + await standardRequest.post( INTEGRATION_HEROKU_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", @@ -209,7 +209,7 @@ const exchangeCodeHeroku = async ({ code }: { code: string }) => { */ const exchangeCodeVercel = async ({ code }: { code: string }) => { const res: ExchangeCodeVercelResponse = ( - await request.post( + await standardRequest.post( INTEGRATION_VERCEL_TOKEN_URL, new URLSearchParams({ code: code, @@ -240,7 +240,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => { */ const exchangeCodeNetlify = async ({ code }: { code: string }) => { const res: ExchangeCodeNetlifyResponse = ( - await request.post( + await standardRequest.post( INTEGRATION_NETLIFY_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", @@ -252,14 +252,14 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => { ) ).data; - const res2 = await request.get("https://api.netlify.com/api/v1/sites", { + const res2 = await standardRequest.get("https://api.netlify.com/api/v1/sites", { headers: { Authorization: `Bearer ${res.access_token}`, }, }); const res3 = ( - await request.get("https://api.netlify.com/api/v1/accounts", { + await standardRequest.get("https://api.netlify.com/api/v1/accounts", { headers: { Authorization: `Bearer ${res.access_token}`, }, @@ -287,7 +287,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => { */ const exchangeCodeGithub = async ({ code }: { code: string }) => { const res: ExchangeCodeGithubResponse = ( - await request.get(INTEGRATION_GITHUB_TOKEN_URL, { + await standardRequest.get(INTEGRATION_GITHUB_TOKEN_URL, { params: { client_id: await getClientIdGitHub(), client_secret: await getClientSecretGitHub(), @@ -321,7 +321,7 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => { const exchangeCodeGitlab = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); const res: ExchangeCodeGitlabResponse = ( - await request.post( + await standardRequest.post( INTEGRATION_GITLAB_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index 823d05e69..0c401bf15 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -1,4 +1,4 @@ -import request from "../config/request"; +import { standardRequest } from "../config/request"; import { IIntegrationAuth } from "../models"; import { INTEGRATION_AZURE_KEY_VAULT, @@ -121,7 +121,7 @@ const exchangeRefreshAzure = async ({ refreshToken: string; }) => { const accessExpiresAt = new Date(); - const { data }: { data: RefreshTokenAzureResponse } = await request.post( + const { data }: { data: RefreshTokenAzureResponse } = await standardRequest.post( INTEGRATION_AZURE_TOKEN_URL, new URLSearchParams({ client_id: await getClientIdAzure(), @@ -158,7 +158,7 @@ const exchangeRefreshHeroku = async ({ data, }: { data: RefreshTokenHerokuResponse; - } = await request.post( + } = await standardRequest.post( INTEGRATION_HEROKU_TOKEN_URL, new URLSearchParams({ grant_type: "refresh_token", @@ -193,7 +193,7 @@ const exchangeRefreshGitLab = async ({ data, }: { data: RefreshTokenGitLabResponse; - } = await request.post( + } = await standardRequest.post( INTEGRATION_GITLAB_TOKEN_URL, new URLSearchParams({ grant_type: "refresh_token", diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index ec55eca77..d659af295 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -37,8 +37,7 @@ import { INTEGRATION_TRAVISCI_API_URL, INTEGRATION_SUPABASE_API_URL } from "../variables"; -import request from '../config/request'; -import axios from "axios"; +import { standardRequest} from '../config/request'; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -215,7 +214,7 @@ const syncSecretsAzureKeyVault = async ({ let result: GetAzureKeyVaultSecret[] = []; try { while (url) { - const res = await request.get(url, { + const res = await standardRequest.get(url, { headers: { Authorization: `Bearer ${accessToken}` } @@ -242,7 +241,7 @@ const syncSecretsAzureKeyVault = async ({ lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/'); } - const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { + const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { headers: { 'Authorization': `Bearer ${accessToken}` } @@ -308,7 +307,7 @@ const syncSecretsAzureKeyVault = async ({ while (!isSecretSet && maxTries > 0) { // try to set secret try { - await request.put( + await standardRequest.put( `${integration.app}/secrets/${key}?api-version=7.3`, { value @@ -325,7 +324,7 @@ const syncSecretsAzureKeyVault = async ({ } catch (err) { const error: any = err; if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') { - await request.post( + await standardRequest.post( `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {}, { headers: { @@ -355,7 +354,7 @@ const syncSecretsAzureKeyVault = async ({ for await (const deleteSecret of deleteSecrets) { const { key } = deleteSecret; - await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { + await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { headers: { 'Authorization': `Bearer ${accessToken}` } @@ -568,7 +567,7 @@ const syncSecretsHeroku = async ({ }) => { try { const herokuSecrets = ( - await request.get( + await standardRequest.get( `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, { headers: { @@ -586,7 +585,7 @@ const syncSecretsHeroku = async ({ } }); - await request.patch( + await standardRequest.patch( `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, secrets, { @@ -642,7 +641,7 @@ const syncSecretsVercel = async ({ : {}), }; - const vercelSecrets: VercelSecret[] = (await request.get( + const vercelSecrets: VercelSecret[] = (await standardRequest.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { params, @@ -675,7 +674,7 @@ const syncSecretsVercel = async ({ for await (const vercelSecret of vercelSecrets) { if (vercelSecret.type === 'encrypted') { // case: secret is encrypted -> need to decrypt - const decryptedSecret = (await request.get( + const decryptedSecret = (await standardRequest.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, { params, @@ -747,7 +746,7 @@ const syncSecretsVercel = async ({ // Sync/push new secrets if (newSecrets.length > 0) { - await request.post( + await standardRequest.post( `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, { @@ -763,7 +762,7 @@ const syncSecretsVercel = async ({ for await (const secret of updateSecrets) { if (secret.type !== 'sensitive') { const { id, ...updatedSecret } = secret; - await request.patch( + await standardRequest.patch( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, updatedSecret, { @@ -778,7 +777,7 @@ const syncSecretsVercel = async ({ } for await (const secret of deleteSecrets) { - await request.delete( + await standardRequest.delete( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, { params, @@ -837,7 +836,7 @@ const syncSecretsNetlify = async ({ }); const res = ( - await request.get( + await standardRequest.get( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, { params: getParams, @@ -951,7 +950,7 @@ const syncSecretsNetlify = async ({ }); if (newSecrets.length > 0) { - await request.post( + await standardRequest.post( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, newSecrets, { @@ -966,7 +965,7 @@ const syncSecretsNetlify = async ({ if (updateSecrets.length > 0) { updateSecrets.forEach(async (secret: NetlifySecret) => { - await request.patch( + await standardRequest.patch( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, { context: secret.values[0].context, @@ -985,7 +984,7 @@ const syncSecretsNetlify = async ({ if (deleteSecrets.length > 0) { deleteSecrets.forEach(async (key: string) => { - await request.delete( + await standardRequest.delete( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, { params: syncParams, @@ -1000,7 +999,7 @@ const syncSecretsNetlify = async ({ if (deleteSecretValues.length > 0) { deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await request.delete( + await standardRequest.delete( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, { params: syncParams, @@ -1151,7 +1150,7 @@ const syncSecretsRender = async ({ accessToken: string; }) => { try { - await request.put( + await standardRequest.put( `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, Object.keys(secrets).map((key) => ({ key, @@ -1203,7 +1202,7 @@ const syncSecretsRailway = async ({ variables: secrets }; - await request.post(INTEGRATION_RAILWAY_API_URL, { + await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { query, variables: { input, @@ -1261,7 +1260,7 @@ const syncSecretsFlyio = async ({ } `; - await request.post(INTEGRATION_FLYIO_API_URL, { + await standardRequest.post(INTEGRATION_FLYIO_API_URL, { query: SetSecrets, variables: { input: { @@ -1296,7 +1295,7 @@ const syncSecretsFlyio = async ({ } }`; - const getSecretsRes = (await request.post(INTEGRATION_FLYIO_API_URL, { + const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, { query: GetSecrets, variables: { appName: integration.app, @@ -1332,7 +1331,7 @@ const syncSecretsFlyio = async ({ } }`; - await request.post(INTEGRATION_FLYIO_API_URL, { + await standardRequest.post(INTEGRATION_FLYIO_API_URL, { query: DeleteSecrets, variables: { input: { @@ -1373,7 +1372,7 @@ const syncSecretsCircleCI = async ({ }) => { try { const circleciOrganizationDetail = ( - await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json", @@ -1386,7 +1385,7 @@ const syncSecretsCircleCI = async ({ // sync secrets to CircleCI Object.keys(secrets).forEach( async (key) => - await request.post( + await standardRequest.post( `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, { name: key, @@ -1403,7 +1402,7 @@ const syncSecretsCircleCI = async ({ // get secrets from CircleCI const getSecretsRes = ( - await request.get( + await standardRequest.get( `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, { headers: { @@ -1417,7 +1416,7 @@ const syncSecretsCircleCI = async ({ // delete secrets from CircleCI getSecretsRes.forEach(async (sec: any) => { if (!(sec.name in secrets)) { - await request.delete( + await standardRequest.delete( `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, { headers: { @@ -1454,7 +1453,7 @@ const syncSecretsTravisCI = async ({ try { // get secrets from travis-ci const getSecretsRes = ( - await request.get( + await standardRequest.get( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, { headers: { @@ -1476,7 +1475,7 @@ const syncSecretsTravisCI = async ({ if (!(key in getSecretsRes)) { // case: secret does not exist in travis ci // -> add secret - await request.post( + await standardRequest.post( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, { env_var: { @@ -1495,7 +1494,7 @@ const syncSecretsTravisCI = async ({ } else { // case: secret exists in travis ci // -> update/set secret - await request.patch( + await standardRequest.patch( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, { env_var: { @@ -1517,7 +1516,7 @@ const syncSecretsTravisCI = async ({ for await (const key of Object.keys(getSecretsRes)) { if (!(key in secrets)){ // delete secret - await request.delete( + await standardRequest.delete( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, { headers: { @@ -1562,7 +1561,7 @@ const syncSecretsGitLab = async ({ // get secrets from gitlab const getSecretsRes: GitLabSecret[] = ( - await request.get( + await standardRequest.get( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, { headers: { @@ -1580,7 +1579,7 @@ const syncSecretsGitLab = async ({ for await (const key of Object.keys(secrets)) { const existingSecret = getSecretsRes.find((s: any) => s.key == key); if (!existingSecret) { - await request.post( + await standardRequest.post( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, { key: key, @@ -1601,7 +1600,7 @@ const syncSecretsGitLab = async ({ } else { // update secret if (secrets[key] !== existingSecret.value) { - await request.put( + await standardRequest.put( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, { ...existingSecret, @@ -1622,7 +1621,7 @@ const syncSecretsGitLab = async ({ // delete secrets for await (const sec of getSecretsRes) { if (!(sec.key in secrets)) { - await request.delete( + await standardRequest.delete( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`, { headers: { @@ -1657,7 +1656,7 @@ const syncSecretsSupabase = async ({ accessToken: string; }) => { try { - const { data: getSecretsRes } = await request.get( + const { data: getSecretsRes } = await standardRequest.get( `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, { headers: { @@ -1677,7 +1676,7 @@ const syncSecretsSupabase = async ({ } ); - await request.post( + await standardRequest.post( `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, modifiedFormatForSecretInjection, { @@ -1695,7 +1694,7 @@ const syncSecretsSupabase = async ({ } }); - await request.delete( + await standardRequest.delete( `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, { headers: { diff --git a/backend/src/integrations/teams.ts b/backend/src/integrations/teams.ts index 3bed93c9a..74fc0ca86 100644 --- a/backend/src/integrations/teams.ts +++ b/backend/src/integrations/teams.ts @@ -5,7 +5,7 @@ import { INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL } from '../variables'; -import request from '../config/request'; +import { standardRequest } from '../config/request'; interface Team { name: string; @@ -56,7 +56,7 @@ const getTeamsGitLab = async ({ accessToken: string; }) => { let teams: Team[] = []; - const res = (await request.get( + const res = (await standardRequest.get( `${INTEGRATION_GITLAB_API_URL}/v4/groups`, { headers: { diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 76f723df2..e8b433cd5 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -31,7 +31,7 @@ const requireWorkspaceAuth = ({ const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; // validate clients - const { membership } = await validateClientForWorkspace({ + const { membership, workspace } = await validateClientForWorkspace({ authData: req.authData, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -43,6 +43,10 @@ const requireWorkspaceAuth = ({ if (membership) { req.membership = membership; } + + if (workspace) { + req.workspace = workspace; + } return next(); }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 2a77c95e9..555f481f8 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -21,6 +21,7 @@ export interface IIntegration { workspace: Types.ObjectId; environment: string; isActive: boolean; + url: string; app: string; appId: string; owner: string; @@ -63,6 +64,11 @@ const integrationSchema = new Schema( type: Boolean, required: true, }, + url: { + // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) + type: String, + default: null + }, app: { // name of app in provider type: String, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2ad78bafb..ad5a3ce02 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -37,6 +37,8 @@ services: - MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin networks: - infisical-dev + extra_hosts: + - "host.docker.internal:host-gateway" frontend: container_name: infisical-dev-frontend From 658df21189c9587842def8428e719dbeb3b16fd0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 00:09:00 -0400 Subject: [PATCH 02/17] Add auto install pre commit --- .../pre-commit-without-bang.sh | 20 +++ .../cmd/pre-commit-script/pre-commit.sh | 20 +++ cli/packages/cmd/scan.go | 163 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh create mode 100644 cli/packages/cmd/pre-commit-script/pre-commit.sh diff --git a/cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh b/cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh new file mode 100644 index 000000000..e47643cc9 --- /dev/null +++ b/cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh @@ -0,0 +1,20 @@ + + +# MANAGED BY INFISICAL CLI (Do not modify): START +infisicalScanEnabled=$(git config --bool hooks.infisical-scan) + +if [ "$infisicalScanEnabled" != "false" ]; then + infisical scan git-changes -v --staged + exitCode=$? + if [ $exitCode -eq 1 ]; then + echo "Commit blocked: Infisical scan has uncovered secrets in your git commit" + echo "To disable the Infisical scan precommit hook run the following command:" + echo "" + echo " git config hooks.infisical-scan false" + echo "" + exit 1 + fi +else + echo 'Warning: infisical scan precommit disabled' +fi +# MANAGED BY INFISICAL CLI (Do not modify): END \ No newline at end of file diff --git a/cli/packages/cmd/pre-commit-script/pre-commit.sh b/cli/packages/cmd/pre-commit-script/pre-commit.sh new file mode 100644 index 000000000..f899a1a51 --- /dev/null +++ b/cli/packages/cmd/pre-commit-script/pre-commit.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# MANAGED BY INFISICAL CLI (Do not modify): START +infisicalScanEnabled=$(git config --bool hooks.infisical-scan) + +if [ "$infisicalScanEnabled" != "false" ]; then + infisical scan git-changes -v --staged + exitCode=$? + if [ $exitCode -eq 1 ]; then + echo "Commit blocked: Infisical scan has uncovered secrets in your git commit" + echo "To disable the Infisical scan precommit hook run the following command:" + echo "" + echo " git config hooks.infisical-scan false" + echo "" + exit 1 + fi +else + echo 'Warning: infisical scan precommit disabled' +fi +# MANAGED BY INFISICAL CLI (Do not modify): END \ No newline at end of file diff --git a/cli/packages/cmd/scan.go b/cli/packages/cmd/scan.go index 45796d29d..cc8cd2e69 100644 --- a/cli/packages/cmd/scan.go +++ b/cli/packages/cmd/scan.go @@ -23,7 +23,11 @@ package cmd import ( + _ "embed" + "fmt" + "io/ioutil" "os" + "os/exec" "path/filepath" "strings" "time" @@ -32,6 +36,7 @@ import ( "github.com/Infisical/infisical-merge/detect" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/report" + "github.com/manifoldco/promptui" "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -45,6 +50,17 @@ order of precedence: 3. (--source/-s)/.infisical-scan.toml If none of the three options are used, then Infisical will use the default scan config` +//go:embed pre-commit-script/pre-commit.sh +var preCommitTemplate []byte + +//go:embed pre-commit-script/pre-commit-without-bang.sh +var preCommitTemplateAppend []byte + +const ( + defaultHooksPath = ".git/hooks/" + preCommitFile = "pre-commit" +) + func init() { // scan flag for only scan command scanCmd.Flags().String("log-opts", "", "git log options") @@ -77,6 +93,9 @@ func init() { // add flags to main scanCmd.AddCommand(scanGitChangesCmd) rootCmd.AddCommand(scanCmd) + + installCmd.Flags().Bool("pre-commit-hook", false, "installs pre commit hook for Git repository") + scanCmd.AddCommand(installCmd) } func initScanConfig(cmd *cobra.Command) { @@ -132,6 +151,50 @@ func initScanConfig(cmd *cobra.Command) { } } +var installCmd = &cobra.Command{ + Use: "install", + Short: "Install scanning scripts and tools. Use --help flag to see all options", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + installPrecommit := cmd.Flags().Changed("pre-commit-hook") + if installPrecommit { + hooksPath, err := getHooksPath() + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + + if hooksPath != ".git/hooks" { + defaultHookOverride, err := overrideDefaultHooksPath(hooksPath) + if err != nil { + fmt.Printf("Error: %s\n", err) + } + + if defaultHookOverride { + ConfigureGitHooksPath() + + log.Info().Msgf("To switch back previous githooks manager run: git config core.hooksPath %s\n", hooksPath) + return + } else { + log.Warn().Msgf("To automatically configure this hook, you need to switch the path of the Hooks. Alternatively, you can manually configure this hook by setting your pre-commit script to run command [infisical scan git-changes -v --staged].\n") + return + } + } + + err = createOrUpdatePreCommitFile(hooksPath) + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + + log.Info().Msgf("Pre-commit hook successfully added. Infisical scan should now run on each commit you make\n") + + Telemetry.CaptureEvent("cli-command:install --pre-commit-hook", posthog.NewProperties().Set("version", util.CLI_VERSION)) + + return + } + }} + var scanCmd = &cobra.Command{ Use: "scan", Short: "Scan for leaked secrets in git history, directories, and files", @@ -417,3 +480,103 @@ func FormatDuration(d time.Duration) string { } return d.Round(scale / 100).String() } + +func overrideDefaultHooksPath(managedHook string) (bool, error) { + YES := "Yes" + NO := "No" + + options := []string{YES, NO} + optionsPrompt := promptui.Select{ + Label: fmt.Sprintf("Your hooks path is set to [%s] but needs to be [.git/hooks] for automatic configuration. Would you like to switch? ", managedHook), + Items: options, + Size: 2, + } + + _, selectedOption, err := optionsPrompt.Run() + if err != nil { + return false, err + } + + return selectedOption == YES, err +} + +func ConfigureGitHooksPath() { + cmd := exec.Command("git", "config", "core.hooksPath", ".git/hooks") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + log.Fatal().Msgf("Failed to configure git hooks path: %v", err) + } +} + +// GetGitRoot returns the root directory of the current Git repository. +func GetGitRoot() (string, error) { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + output, err := cmd.Output() + + if err != nil { + return "", fmt.Errorf("failed to get git root directory: %w", err) + } + + gitRoot := strings.TrimSpace(string(output)) // Remove any trailing newline + return gitRoot, nil +} + +func getHooksPath() (string, error) { + out, err := exec.Command("git", "config", "core.hooksPath").Output() + if err != nil { + return "", fmt.Errorf("failed to get Git hooks path: %s", err) + } + + hooksPath := strings.TrimSpace(string(out)) + return hooksPath, nil +} + +func createOrUpdatePreCommitFile(hooksPath string) error { + // File doesn't exist, create a new one + rootGitRepoPath, err := GetGitRoot() + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s/%s/%s", rootGitRepoPath, hooksPath, preCommitFile) + + _, err = os.Stat(filePath) + if err == nil { + // File already exists, check if it contains the managed comments + content, err := ioutil.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read pre-commit file: %s", err) + } + + if strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): START") && + strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): END") { + return nil + } + + // File already exists, append the template content + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0755) + if err != nil { + return fmt.Errorf("failed to open pre-commit file: %s", err) + } + + defer file.Close() + + _, err = file.Write(preCommitTemplateAppend) + if err != nil { + return fmt.Errorf("failed to append to pre-commit file: %s", err) + } + + } else if os.IsNotExist(err) { + err = os.WriteFile(filePath, preCommitTemplate, 0755) + if err != nil { + return fmt.Errorf("failed to create pre-commit file: %s", err) + } + } else { + // Error occurred while checking file status + return fmt.Errorf("failed to check pre-commit file status: %s", err) + } + + return nil +} From 899d46514cfd7b57bb48882b95a4e6d466953d05 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 23 May 2023 16:59:13 +0300 Subject: [PATCH 03/17] Add forwarding usedSeats and subscription quantity to license server on org member add/delete --- .../controllers/v1/membershipOrgController.ts | 5 ++ .../src/controllers/v2/signupController.ts | 27 +++++++- backend/src/helpers/organization.ts | 61 ++++++++++++------- .../src/middleware/requireWorkspaceAuth.ts | 2 - 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index e5714516e..60cdc9691 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -135,6 +135,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { } if (!inviteeMembershipOrg) { + await new MembershipOrg({ user: invitee, inviteEmail: inviteeEmail, @@ -246,6 +247,10 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { // membership can be approved and redirected to login/dashboard membershipOrg.status = ACCEPTED; await membershipOrg.save(); + + await updateSubscriptionOrgQuantity({ + organizationId + }); return res.status(200).send({ message: 'Successfully verified email', diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index 28d40403c..d9e9a0447 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -9,6 +9,7 @@ import { issueAuthTokens } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; import { standardRequest } from '../../config/request'; import { getLoopsApiKey, getHttpsEnabled } from '../../config'; +import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; /** * Complete setting up user by adding their personal and auth information as part of the @@ -87,6 +88,19 @@ export const completeAccountSignup = async (req: Request, res: Response) => { user }); + // update organization membership statuses that are + // invited to completed with user attached + const membershipsToUpdate = await MembershipOrg.find({ + inviteEmail: email, + status: INVITED + }); + + membershipsToUpdate.forEach(async (membership) => { + await updateSubscriptionOrgQuantity({ + organizationId: membership.organization.toString() + }); + }); + // update organization membership statuses that are // invited to completed with user attached await MembershipOrg.updateMany( @@ -206,9 +220,20 @@ export const completeAccountInvite = async (req: Request, res: Response) => { if (!user) throw new Error('Failed to complete account for non-existent user'); - + // update organization membership statuses that are // invited to completed with user attached + const membershipsToUpdate = await MembershipOrg.find({ + inviteEmail: email, + status: INVITED + }); + + membershipsToUpdate.forEach(async (membership) => { + await updateSubscriptionOrgQuantity({ + organizationId: membership.organization.toString() + }); + }); + await MembershipOrg.updateMany( { inviteEmail: email, diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 9e67ebb00..43b1bea83 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -29,6 +29,16 @@ import { } from "../utils/errors"; import { validateUserClientForOrganization } from "../helpers/user"; import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount"; +import { + EELicenseService +} from '../ee/services'; +import { + getLicenseServerUrl +} from '../config'; +import { + licenseServerKeyRequest, + licenseKeyRequest +} from '../config/request'; /** * Validate accepted clients for organization with id [organizationId] @@ -228,30 +238,35 @@ const updateSubscriptionOrgQuantity = async ({ }); if (organization && organization.customerId) { - const quantity = await MembershipOrg.countDocuments({ - organization: organizationId, - status: ACCEPTED, - }); - - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: "2022-08-01", - }); - - const subscription = ( - await stripe.subscriptions.list({ - customer: organization.customerId, - }) - ).data[0]; - - stripeSubscription = await stripe.subscriptions.update(subscription.id, { - items: [ + if (EELicenseService.instanceType === 'cloud') { + // instance of Infisical is a cloud instance + const quantity = await MembershipOrg.countDocuments({ + organization: new Types.ObjectId(organizationId), + status: ACCEPTED, + }); + + await licenseServerKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`, { - id: subscription.items.data[0].id, - price: subscription.items.data[0].price.id, - quantity, - }, - ], - }); + quantity + } + ); + } + + if (EELicenseService.instanceType === 'enterprise-self-hosted') { + // instance of Infisical is an enterprise self-hosted instance + + const usedSeats = await MembershipOrg.countDocuments({ + status: ACCEPTED + }); + + await licenseKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license/v1/license`, + { + usedSeats + } + ); + } } return stripeSubscription; diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index e8b433cd5..7e97b2e32 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,8 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { validateMembership } from '../helpers/membership'; import { validateClientForWorkspace } from '../helpers/workspace'; -import { UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; From ec34572087b8d9acdd03cc05b8bf22f8b6d9a01e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 13:17:58 -0400 Subject: [PATCH 04/17] patch invite only --- backend/src/config/index.ts | 2 +- backend/src/controllers/v1/signupController.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 7d46580a1..f7555248a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -5,7 +5,7 @@ const client = new InfisicalClient({ }); export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000; -export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue; +export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true' export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue; export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10; export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d'; diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 193699c15..e5af676b3 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -21,14 +21,6 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; - if (await getInviteOnlySignup()) { - // Only one user can create an account without being invited. The rest need to be invited in order to make an account - const userCount = await User.countDocuments({}) - if (userCount != 0) { - throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) - } - } - const user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account @@ -74,6 +66,14 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { }); } + if (await getInviteOnlySignup()) { + // Only one user can create an account without being invited. The rest need to be invited in order to make an account + const userCount = await User.countDocuments({}) + if (userCount != 0) { + throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) + } + } + // verify email if (await getSmtpConfigured()) { await checkEmailVerification({ From b5b2f402ad65dcd8e00da5697d3d04a2b890c3df Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 14:09:45 -0400 Subject: [PATCH 05/17] add missing required envrs --- .../self-hosting/deployment-options/standalone-infisical.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 61cdac62f..8974ae4e6 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -21,6 +21,11 @@ docker pull infisical/infisical:latest The Infisical Docker image requires a .env file to manage environment variables. Create a new file called .env in your preferred location. Add the required environment variables listed below. View [all configurable environment variables](../configuration/envars) + + + Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` + + Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` From 83dd35299c48c26ade1e0d7e255b6831db84aa66 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 23 May 2023 22:28:41 +0300 Subject: [PATCH 06/17] Added add/remove/get organization payment methods and get cloud plans from license server --- .../controllers/v1/cloudProductsController.ts | 34 ++++++++ backend/src/ee/controllers/v1/index.ts | 4 +- .../controllers/v1/organizationsController.ts | 81 +++++++++++++++++-- backend/src/ee/helpers/organizations.ts | 39 --------- backend/src/ee/routes/v1/cloudProducts.ts | 20 +++++ backend/src/ee/routes/v1/index.ts | 4 +- backend/src/ee/routes/v1/organizations.ts | 46 ++++++++++- backend/src/index.ts | 4 +- 8 files changed, 182 insertions(+), 50 deletions(-) create mode 100644 backend/src/ee/controllers/v1/cloudProductsController.ts delete mode 100644 backend/src/ee/helpers/organizations.ts create mode 100644 backend/src/ee/routes/v1/cloudProducts.ts diff --git a/backend/src/ee/controllers/v1/cloudProductsController.ts b/backend/src/ee/controllers/v1/cloudProductsController.ts new file mode 100644 index 000000000..54584ce82 --- /dev/null +++ b/backend/src/ee/controllers/v1/cloudProductsController.ts @@ -0,0 +1,34 @@ +import * as Sentry from '@sentry/node'; +import { Request, Response } from 'express'; +import { EELicenseService } from '../../services'; +import { getLicenseServerUrl } from '../../../config'; +import { licenseServerKeyRequest } from '../../../config/request'; + +/** + * Return available cloud product information. + * Note: Nicely formatted to easily construct a table from + * @param req + * @param res + * @returns + */ +export const getCloudProducts = async (req: Request, res: Response) => { + try { + const billingCycle = req.query['billing-cycle'] as string; + + if (EELicenseService.instanceType === 'cloud') { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); + + return res.status(200).send(data); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + } + + return res.status(200).send({ + head: [], + rows: [] + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index b9618495f..bf3992b17 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -5,6 +5,7 @@ import * as organizationsController from './organizationsController'; import * as workspaceController from './workspaceController'; import * as actionController from './actionController'; import * as membershipController from './membershipController'; +import * as cloudProductsController from './cloudProductsController'; export { stripeController, @@ -13,5 +14,6 @@ export { organizationsController, workspaceController, actionController, - membershipController + membershipController, + cloudProductsController } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 1f9cf1080..1a19eef2d 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -1,15 +1,82 @@ -import { Types } from 'mongoose'; +import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; -import { getOrganizationPlanHelper } from '../../helpers/organizations'; +import { getLicenseServerUrl } from '../../../config'; +import { licenseServerKeyRequest } from '../../../config/request'; +import { EELicenseService } from '../../services'; +/** + * Return the organization's current plan and allowed feature set + */ export const getOrganizationPlan = async (req: Request, res: Response) => { - const { organizationId } = req.params; + try { + if (EELicenseService.instanceType === 'cloud') { + // instance of Infisical is a cloud instance - const plan = await getOrganizationPlanHelper({ - organizationId: new Types.ObjectId(organizationId) + const cachedPlan = EELicenseService.localFeatureSet.get(req.organization._id.toString()); + if (cachedPlan) return cachedPlan; + + const { data: { currentPlan } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan` + ); + + // cache fetched plan for organization + EELicenseService.localFeatureSet.set(req.organization._id.toString(), currentPlan); + + return res.status(200).send({ + plan: currentPlan + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + } + + return res.status(200).send({ + plan: EELicenseService.globalFeatureSet }); +} + +/** + * Return the organization's payment methods on file + */ +export const getOrganizationPmtMethods = async (req: Request, res: Response) => { + const { data: { pmtMethods } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` + ); + + return res.status(200).send({ + pmtMethods + }); +} + +/** + * Return a Stripe session URL to add payment method for organization + */ +export const addOrganizationPmtMethod = async (req: Request, res: Response) => { + const { + success_url, + cancel_url + } = req.body; + + const { data: { url } } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, + { + success_url, + cancel_url + } + ); return res.status(200).send({ - plan - }); + url + }); +} + +export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { + const { pmtMethodId } = req.params; + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, + ); + + return res.status(200).send(data); } \ No newline at end of file diff --git a/backend/src/ee/helpers/organizations.ts b/backend/src/ee/helpers/organizations.ts deleted file mode 100644 index 80b1048be..000000000 --- a/backend/src/ee/helpers/organizations.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; -import { Organization } from '../../models'; -import { EELicenseService } from '../services'; -import { getLicenseServerUrl } from '../../config'; -import { licenseServerKeyRequest } from '../../config/request'; -import { OrganizationNotFoundError } from '../../utils/errors'; - -export const getOrganizationPlanHelper = async ({ - organizationId -}: { - organizationId: Types.ObjectId; -}) => { - try { - if (EELicenseService.instanceType === 'cloud') { - // instance of Infisical is a cloud instance - - const organization = await Organization.findById(organizationId); - if (!organization) throw OrganizationNotFoundError(); - - const cachedPlan = EELicenseService.localFeatureSet.get(organizationId.toString()); - if (cachedPlan) return cachedPlan; - - const { data: { currentPlan } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` - ); - - // cache fetched plan for organization - EELicenseService.localFeatureSet.set(organizationId.toString(), currentPlan); - return currentPlan; - } - - return EELicenseService.globalFeatureSet; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return EELicenseService.globalFeatureSet; - } -} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/cloudProducts.ts b/backend/src/ee/routes/v1/cloudProducts.ts new file mode 100644 index 000000000..73af00aca --- /dev/null +++ b/backend/src/ee/routes/v1/cloudProducts.ts @@ -0,0 +1,20 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + validateRequest +} from '../../../middleware'; +import { query } from 'express-validator'; +import { cloudProductsController } from '../../controllers/v1'; + +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + query('billing-cycle').exists().isIn(['monthly', 'yearly']), + validateRequest, + cloudProductsController.getCloudProducts +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 9b4d30b1b..7568e45d6 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -3,11 +3,13 @@ import secretSnapshot from './secretSnapshot'; import organizations from './organizations'; import workspace from './workspace'; import action from './action'; +import cloudProducts from './cloudProducts'; export { secret, secretSnapshot, organizations, workspace, - action + action, + cloudProducts } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 3f208b3c2..b41e49334 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -5,7 +5,7 @@ import { requireOrganizationAuth, validateRequest } from '../../../middleware'; -import { param } from 'express-validator'; +import { param, body } from 'express-validator'; import { organizationsController } from '../../controllers/v1'; import { OWNER, ADMIN, MEMBER, ACCEPTED @@ -25,4 +25,48 @@ router.get( organizationsController.getOrganizationPlan ); +router.get( + '/:organizationId/billing-details/payment-methods', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + validateRequest, + organizationsController.getOrganizationPmtMethods +); + +router.post( + '/:organizationId/billing-details/payment-methods', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + body('success_url').exists().isString(), + body('cancel_url').exists().isString(), + validateRequest, + organizationsController.addOrganizationPmtMethod +); + +router.delete( + '/:organizationId/billing-details/payment-methods/:pmtMethodId', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + validateRequest, + organizationsController.deleteOrganizationPmtMethod +); + export default router; \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index c4ba7c846..5906cd5a1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -26,7 +26,8 @@ import { secret as eeSecretRouter, secretSnapshot as eeSecretSnapshotRouter, action as eeActionRouter, - organizations as eeOrganizationsRouter + organizations as eeOrganizationsRouter, + cloudProducts as eeCloudProductsRouter } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -122,6 +123,7 @@ const main = async () => { app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/action', eeActionRouter); app.use('/api/v1/organizations', eeOrganizationsRouter); + app.use('/api/v1/cloud-products', eeCloudProductsRouter); // v1 routes (default) app.use('/api/v1/signup', v1SignupRouter); From a8502377c7187ef09e56ef2be0ee795ad27f6614 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 23 May 2023 23:14:20 +0300 Subject: [PATCH 07/17] Add endpoint for updating organization plan --- .../controllers/v1/organizationsController.ts | 21 +++++++++++++++++++ backend/src/ee/routes/v1/organizations.ts | 15 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 1a19eef2d..b79379f29 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -36,6 +36,27 @@ export const getOrganizationPlan = async (req: Request, res: Response) => { }); } +/** + * Update the organization plan to product with id [productId] + * @param req + * @param res + * @returns + */ +export const updateOrganizationPlan = async (req: Request, res: Response) => { + const { + productId + } = req.body; + + const { data } = await licenseServerKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`, + { + productId + } + ); + + return res.status(200).send(data); +} + /** * Return the organization's payment methods on file */ diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index b41e49334..fd9fd08e9 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -25,6 +25,21 @@ router.get( organizationsController.getOrganizationPlan ); +router.patch( + '/:organizationId/plan', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + body('productId').exists().isString(), + validateRequest, + organizationsController.updateOrganizationPlan +); + router.get( '/:organizationId/billing-details/payment-methods', requireAuth({ From 96607153dccafacb1d465dd3375fba19d778ec5c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 23 May 2023 23:54:54 +0300 Subject: [PATCH 08/17] Modularize getOrganizationPlan function --- .../controllers/v1/organizationsController.ts | 27 +++---------------- backend/src/ee/services/EELicenseService.ts | 25 +++++++++++++++++ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index b79379f29..0bdf6e435 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import { getLicenseServerUrl } from '../../../config'; import { licenseServerKeyRequest } from '../../../config/request'; @@ -8,31 +7,13 @@ import { EELicenseService } from '../../services'; * Return the organization's current plan and allowed feature set */ export const getOrganizationPlan = async (req: Request, res: Response) => { - try { - if (EELicenseService.instanceType === 'cloud') { - // instance of Infisical is a cloud instance + const plan = await EELicenseService.getOrganizationPlan(req.organization._id.toString()); - const cachedPlan = EELicenseService.localFeatureSet.get(req.organization._id.toString()); - if (cachedPlan) return cachedPlan; - - const { data: { currentPlan } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan` - ); - - // cache fetched plan for organization - EELicenseService.localFeatureSet.set(req.organization._id.toString(), currentPlan); - - return res.status(200).send({ - plan: currentPlan - }); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - } + // cache fetched plan for organization + EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan); return res.status(200).send({ - plan: EELicenseService.globalFeatureSet + plan }); } diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 02cab7e5d..7c12931f0 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -7,9 +7,12 @@ import { } from '../../config'; import { licenseKeyRequest, + licenseServerKeyRequest, refreshLicenseServerKeyToken, refreshLicenseKeyToken } from '../../config/request'; +import { Organization } from '../../models'; +import { OrganizationNotFoundError } from '../../utils/errors'; interface FeatureSet { _id: string | null; @@ -59,6 +62,28 @@ class EELicenseService { stdTTL: 300 }); } + + public async getOrganizationPlan(organizationId: string) { + try { + if (this.instanceType === 'cloud') { + const cachedPlan = this.localFeatureSet.get(organizationId); + if (cachedPlan) return cachedPlan; + + const organization = await Organization.findById(organizationId); + if (!organization) throw OrganizationNotFoundError(); + + const { data: { currentPlan } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` + ); + + return currentPlan; + } + } catch (err) { + return this.globalFeatureSet; + } + + return this.globalFeatureSet; + } public async initGlobalFeatureSet() { const licenseServerKey = await getLicenseServerKey(); From d2e3aa15b0e748cc3d8e7cdf60b59d46580ee962 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 17:16:24 -0400 Subject: [PATCH 09/17] patch standalone docker image --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..6a47c2536 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +backend/node_modules +frontend/node_modules \ No newline at end of file From 01385687e04e3e2f68ae084a16b6723be6d4d4a1 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 18:16:13 -0400 Subject: [PATCH 10/17] make posthog failed calls level=debug --- cli/packages/telemetry/telemetry.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cli/packages/telemetry/telemetry.go b/cli/packages/telemetry/telemetry.go index 18136e909..ffd743457 100644 --- a/cli/packages/telemetry/telemetry.go +++ b/cli/packages/telemetry/telemetry.go @@ -4,6 +4,7 @@ import ( "github.com/Infisical/infisical-merge/packages/util" "github.com/denisbrodbeck/machineid" "github.com/posthog/posthog-go" + "github.com/rs/zerolog/log" ) var POSTHOG_API_KEY_FOR_CLI string @@ -13,11 +14,23 @@ type Telemetry struct { posthogClient posthog.Client } +type NoOpLogger struct{} + +func (NoOpLogger) Logf(format string, args ...interface{}) { + log.Debug().Msgf(format, args...) +} + +func (NoOpLogger) Errorf(format string, args ...interface{}) { + log.Debug().Msgf(format, args...) +} + func NewTelemetry(telemetryIsEnabled bool) *Telemetry { if POSTHOG_API_KEY_FOR_CLI != "" { client, _ := posthog.NewWithConfig( POSTHOG_API_KEY_FOR_CLI, - posthog.Config{}, + posthog.Config{ + Logger: NoOpLogger{}, + }, ) return &Telemetry{isEnabled: telemetryIsEnabled, posthogClient: client} From 1d17596af19db95a56b237d02383629f7c90ad95 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Tue, 23 May 2023 15:41:34 -0700 Subject: [PATCH 11/17] added boolean flag for the plan in posthog logging --- .../src/controllers/v2/secretsController.ts | 65 ++++++++++++++++--- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 51c93182b..2969e84cb 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,7 +1,7 @@ import to from 'await-to-js'; import { Types } from 'mongoose'; import { Request, Response } from 'express'; -import { ISecret, Secret } from '../../models'; +import { ISecret, Secret, Workspace } from '../../models'; import { IAction, SecretVersion } from '../../ee/models'; import { SECRET_PERSONAL, @@ -14,7 +14,7 @@ import { import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; -import { EESecretService, EELogService } from '../../ee/services'; +import { EESecretService, EELogService, EELicenseService } from '../../ee/services'; import { TelemetryService, SecretService } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { PERMISSION_WRITE_SECRETS } from '../../variables'; @@ -48,6 +48,14 @@ export const batchSecrets = async (req: Request, res: Response) => { environment: string; requests: BatchSecretRequest[]; } = req.body; + + const organizationId = ( + await Workspace.findOne({ + _id: workspaceId + }) + )?.organization?.toString(); + const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); + const isPaid = orgPlan.slug != 'starter'; const createSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = []; @@ -139,7 +147,8 @@ export const batchSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } @@ -226,7 +235,8 @@ export const batchSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } @@ -261,7 +271,8 @@ export const batchSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel: channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } @@ -376,6 +387,14 @@ export const createSecrets = async (req: Request, res: Response) => { } } + const organizationId = ( + await Workspace.findOne({ + _id: workspaceId + }) + )?.organization?.toString(); + const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); + const isPaid = orgPlan.slug != 'starter'; + let listOfSecretsToCreate; if (Array.isArray(req.body.secrets)) { // case: create multiple secrets @@ -531,7 +550,8 @@ export const createSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel: channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } @@ -595,6 +615,14 @@ export const getSecrets = async (req: Request, res: Response) => { const normalizedPath = normalizePath(secretsPath as string) const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath) + const organizationId = ( + await Workspace.findOne({ + _id: workspaceId + }) + )?.organization?.toString(); + const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); + const isPaid = orgPlan.slug != 'starter'; + // secrets to return let secrets: ISecret[] = []; @@ -727,7 +755,8 @@ export const getSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } @@ -938,6 +967,14 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key) }) + const organizationId = ( + await Workspace.findOne({ + _id: key + }) + )?.organization?.toString(); + const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); + const isPaid = orgPlan.slug != 'starter'; + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ @@ -950,7 +987,8 @@ export const updateSecrets = async (req: Request, res: Response) => { environment: workspaceSecretObj[key][0].environment, workspaceId: key, channel: channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } @@ -1072,6 +1110,14 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key) }); + const organizationId = ( + await Workspace.findOne({ + _id: key + }) + )?.organization?.toString(); + const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); + const isPaid = orgPlan.slug != 'starter'; + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ @@ -1084,7 +1130,8 @@ export const deleteSecrets = async (req: Request, res: Response) => { environment: workspaceSecretObj[key][0].environment, workspaceId: key, channel: channel, - userAgent: req.headers?.['user-agent'] + userAgent: req.headers?.['user-agent'], + isPaid } }); } From f2de1778cb8e71ab58d45588d6b47a09c22d52fe Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 19:34:31 -0400 Subject: [PATCH 12/17] catch case when hook path is default --- cli/packages/cmd/scan.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/packages/cmd/scan.go b/cli/packages/cmd/scan.go index cc8cd2e69..1226e3319 100644 --- a/cli/packages/cmd/scan.go +++ b/cli/packages/cmd/scan.go @@ -526,7 +526,11 @@ func GetGitRoot() (string, error) { func getHooksPath() (string, error) { out, err := exec.Command("git", "config", "core.hooksPath").Output() if err != nil { - return "", fmt.Errorf("failed to get Git hooks path: %s", err) + if len(out) == 0 { + out = []byte(".git/hooks") // set the default hook + } else { + log.Error().Msgf("Failed to get Git hooks path: %s\nOutput: %s\n", err, out) + } } hooksPath := strings.TrimSpace(string(out)) From a7fb0786f9ae45c5bb3a4d261eff42d85aca9a0e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 19:45:10 -0400 Subject: [PATCH 13/17] improve pre commit docs --- docs/cli/commands/scan-install.mdx | 23 +++++++++++++++++++++++ docs/cli/scanning-overview.mdx | 24 ++++++++++++++++++++++++ docs/mint.json | 3 ++- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 docs/cli/commands/scan-install.mdx diff --git a/docs/cli/commands/scan-install.mdx b/docs/cli/commands/scan-install.mdx new file mode 100644 index 000000000..a0128ef18 --- /dev/null +++ b/docs/cli/commands/scan-install.mdx @@ -0,0 +1,23 @@ +--- +title: "scan install" +description: "Add various scanning tools seamlessly into your development lifecycle" +--- + +```bash +infisical scan install --pre-commit-hook +``` + +## Description +The command `infisical scan install` is designed to incorporate various scanning tools seamlessly into your development lifecycle. +Initially, we are offering users the ability to install a pre-commit hook. This hook conducts an automatic scan for any exposed secrets in your commits before they are pushed. + +### Flags + + ```bash + infisical scan install --pre-commit-hook + ``` + + **Description** + Installs a git pre-commit hook that triggers Infisical to scan your staged changes for any exposed secrets prior to pushing. + + \ No newline at end of file diff --git a/docs/cli/scanning-overview.mdx b/docs/cli/scanning-overview.mdx index 57504b1b4..4ff2683a6 100644 --- a/docs/cli/scanning-overview.mdx +++ b/docs/cli/scanning-overview.mdx @@ -52,6 +52,30 @@ In addition to scanning for past leaks, this new addition also actively aids in +# +# +# Automatically scan changes before you commit + +To lower the risk of committing hardcoded secrets to your code repository, we have designed a custom git pre-commit hook. +This hook scans the changes you're about to commit for any exposed secrets. If any hardcoded secrets are detected, it will block your commit. + +### Install pre-commit hook + +To install this git hook, go into your local git repository and run the following command. + +```bash +infisical scan install --pre-commit-hook +``` + +To disable this hook after installing it, run the command `git config --bool hooks.infisical-scan false` + +### Third party hooks management +If you prefer to manage your pre-commit hook outside of the .git/hooks directory, you can easily accomplish this by adding the following command to your pre-commit script + +```bash +infisical scan git-changes --staged --verbose +``` + # # # Creating a baseline diff --git a/docs/mint.json b/docs/mint.json index 7993c2769..a0612889d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -154,7 +154,8 @@ "group": "infisical scan", "pages": [ "cli/commands/scan", - "cli/commands/scan-git-changes" + "cli/commands/scan-git-changes", + "cli/commands/scan-install" ] } ] From 8b05ce11f72d8ddbab90d9980eb758012ba3f4bc Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 20:15:39 -0400 Subject: [PATCH 14/17] add pre commit to husky --- .husky/pre-commit | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index 0b3d59a18..e235e5d79 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,3 +3,5 @@ . "$(dirname -- "$0")/_/husky.sh" npx lint-staged + +infisical scan git-changes --staged -v From d0d6419d4d2ca6388cf1edac38220c55745dc239 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 20:20:10 -0400 Subject: [PATCH 15/17] add pre commit install command to README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30dda9d14..b180a23cd 100644 --- a/README.md +++ b/README.md @@ -104,10 +104,10 @@ To scan your uncommitted git changes, run: infisical scan git-changes --verbose ``` -You can also scan your uncommited but staged changes by running the command below. This command can also be used as a pre-commit hook to prevent secret leak. +Install pre commit hook to scan each commit before you push to your repository ``` -infisical scan git-changes --staged --verbose +infisical scan install --pre-commit-hook ``` Lean about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview) From 9d40a96633818bc1be7e932ff7b2ccaaea0e4fb9 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 23 May 2023 20:22:01 -0400 Subject: [PATCH 16/17] Update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index b180a23cd..1882f7f56 100644 --- a/README.md +++ b/README.md @@ -98,12 +98,6 @@ To scan your full git history, run: infisical scan --verbose ``` -To scan your uncommitted git changes, run: - -``` -infisical scan git-changes --verbose -``` - Install pre commit hook to scan each commit before you push to your repository ``` From e65c6568e100383397ae2fa10129ec4434cce208 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 24 May 2023 10:26:06 +0300 Subject: [PATCH 17/17] Modify convention for PostHog isPaid attr to be tier-based instead of slug --- .../src/controllers/v2/secretsController.ts | 54 ++++++++----------- backend/src/ee/services/EELicenseService.ts | 2 +- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 2969e84cb..4e0bd0488 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,17 +1,15 @@ -import to from 'await-to-js'; import { Types } from 'mongoose'; import { Request, Response } from 'express'; import { ISecret, Secret, Workspace } from '../../models'; import { IAction, SecretVersion } from '../../ee/models'; import { SECRET_PERSONAL, - SECRET_SHARED, ACTION_ADD_SECRETS, ACTION_READ_SECRETS, ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS } from '../../variables'; -import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; +import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors'; import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService, EELicenseService } from '../../ee/services'; @@ -20,7 +18,7 @@ import { getChannelFromUserAgent } from '../../utils/posthog'; import { PERMISSION_WRITE_SECRETS } from '../../variables'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import Tag from '../../models/tag'; -import _, { eq } from 'lodash'; +import _ from 'lodash'; import { BatchSecretRequest, BatchSecret @@ -49,13 +47,11 @@ export const batchSecrets = async (req: Request, res: Response) => { requests: BatchSecretRequest[]; } = req.body; - const organizationId = ( - await Workspace.findOne({ - _id: workspaceId - }) - )?.organization?.toString(); - const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); - const isPaid = orgPlan.slug != 'starter'; + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw WorkspaceNotFoundError(); + + const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); + const isPaid = orgPlan.tier < 1; const createSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = []; @@ -387,13 +383,11 @@ export const createSecrets = async (req: Request, res: Response) => { } } - const organizationId = ( - await Workspace.findOne({ - _id: workspaceId - }) - )?.organization?.toString(); - const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); - const isPaid = orgPlan.slug != 'starter'; + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw WorkspaceNotFoundError(); + + const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); + const isPaid = orgPlan.tier < 1; let listOfSecretsToCreate; if (Array.isArray(req.body.secrets)) { @@ -615,13 +609,11 @@ export const getSecrets = async (req: Request, res: Response) => { const normalizedPath = normalizePath(secretsPath as string) const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath) - const organizationId = ( - await Workspace.findOne({ - _id: workspaceId - }) - )?.organization?.toString(); - const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); - const isPaid = orgPlan.slug != 'starter'; + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw WorkspaceNotFoundError(); + + const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); + const isPaid = orgPlan.tier < 1; // secrets to return let secrets: ISecret[] = []; @@ -967,13 +959,11 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key) }) - const organizationId = ( - await Workspace.findOne({ - _id: key - }) - )?.organization?.toString(); - const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); - const isPaid = orgPlan.slug != 'starter'; + const workspace = await Workspace.findById(key); + if (!workspace) throw WorkspaceNotFoundError(); + + const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); + const isPaid = orgPlan.tier < 1; const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 7c12931f0..38530dfa0 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -43,7 +43,7 @@ class EELicenseService { public globalFeatureSet: FeatureSet = { _id: null, slug: null, - tier: null, + tier: -1, projectLimit: null, memberLimit: null, secretVersioning: true,