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