1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-27 09:40:45 +00:00

Compare commits

..

11 Commits

275 changed files with 9502 additions and 11763 deletions
.env.exampleREADME.md
backend
cli
cloudformation/ec2-deployment
docker-compose.dev.yml
docs
frontend
next-i18next.config.jspackage-lock.jsonpackage.json
public
src
components
helpers
hooks/api
layouts/AppLayout
AppLayout.tsx
components/NavBar
pages
views/Settings
OrgSettingsPage
OrgSettingsPage.tsx
components
OrgIncidentContactsTable
OrgMembersTable
PersonalSettingsPage/SecuritySection
ProjectSettingsPage
tsconfig.json
helm-charts/secrets-operator
i18n
k8-operator
nginx

@ -16,6 +16,9 @@ JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME=
# Optional lifetimes for OTP expressed in seconds
EMAIL_TOKEN_LIFETIME=
# MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
# to the MongoDB container instance or Mongo Cloud
@ -45,12 +48,10 @@ CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors

@ -25,7 +25,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-55.7k-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-36.7k-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@ -44,8 +44,6 @@
<kbd>[<img title="Turkish" alt="Turkish language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/tr.svg" width="22">](i18n/README.tr.md)</kbd>
<kbd>[<img title="Bahasa Indonesia" alt="Bahasa Indonesia language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/id.svg" width="22">](i18n/README.id.md)</kbd>
<kbd>[<img title="Portuguese - Brazil" alt="Portuguese - Brazil" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/br.svg" width="22">](i18n/README.pt-br.md)</kbd>
<kbd>[<img title="Japanese" alt="Japanese language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/jp.svg" width="22">](i18n/README.ja.md)</kbd>
<kbd>[<img title="Italian" alt="Italian language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/it.svg" width="22">](i18n/README.it.md)</kbd>
**[Infisical](https://infisical.com)** is an open source, end-to-end encrypted secret manager which you can use to centralize your API keys and configs. From Infisical, you can then distribute these secrets across your whole development lifecycle - from development to production . It's designed to be simple and take minutes to get going.
@ -57,13 +55,14 @@
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** - manage secrets via HTTPS requests to the platform
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** to view the change history for any secret
- **[Audit Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
- **Role-based Access Controls** per environment
- **2FA** (more options coming soon)
- **Smart Security Alerts**
- 🔜 **1-Click Deploy** to AWS
- 🔜 **Automatic Secret Rotation**
- 🔜 **Smart Security Alerts**
- 🔜 **Secrets Rotation**
- 🔜 **Slack & MS Teams** integrations
And more.
@ -158,9 +157,7 @@ We're currently setting the foundation and building [integrations](https://infis
</a>
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cicd/circleci?ref=github.com">
✔️ CircleCI
</a>
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
</td>
</tr>
<tr>
@ -171,9 +168,7 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 Digital Ocean
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/azure-key-vault?ref=github.com">
✔️ Azure Key Vault
</a>
🔜 Azure
</td>
</tr>
<tr>

@ -0,0 +1,19 @@
import { server } from '../src/app';
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
import supertest from 'supertest';
import { setUpHealthEndpoint } from '../src/services/health';
const requestWithSupertest = supertest(server);
describe('Healthcheck endpoint', () => {
beforeAll(async () => {
setUpHealthEndpoint(server);
});
afterAll(async () => {
server.close();
});
it('GET /healthcheck should return OK', async () => {
const res = await requestWithSupertest.get('/healthcheck');
expect(res.status).toEqual(200);
});
});

@ -4,6 +4,7 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
PORT: string;
EMAIL_TOKEN_LIFETIME: string;
ENCRYPTION_KEY: string;
SALT_ROUNDS: string;
JWT_AUTH_LIFETIME: string;
@ -21,12 +22,10 @@ declare global {
CLIENT_ID_VERCEL: string;
CLIENT_ID_NETLIFY: string;
CLIENT_ID_GITHUB: string;
CLIENT_ID_GITLAB: string;
CLIENT_SECRET_HEROKU: string;
CLIENT_SECRET_VERCEL: string;
CLIENT_SECRET_NETLIFY: string;
CLIENT_SECRET_GITHUB: string;
CLIENT_SECRET_GITLAB: string;
CLIENT_SLUG_VERCEL: string;
POSTHOG_HOST: string;
POSTHOG_PROJECT_API_KEY: string;

@ -1,9 +0,0 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'],
modulePaths: ['<rootDir>/src'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
setupFiles: ['<rootDir>/test-resources/env-vars.js'],
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.ts']
};

10165
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,14 +1,14 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.281.0",
"@aws-sdk/client-secrets-manager": "^3.267.0",
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.40.0",
"@sentry/tracing": "^7.39.0",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1348.0",
"aws-sdk": "^2.1311.0",
"axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
@ -23,21 +23,20 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.0.37",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.1",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.5.4",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.2",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
@ -57,7 +56,7 @@
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles",
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
@ -88,7 +87,7 @@
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"cross-env": "^7.0.3",
"eslint": "^8.26.0",
@ -101,6 +100,17 @@
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"collectCoverageFrom": [
"src/*.{js,ts}",
"!**/node_modules/**"
],
"setupFiles": [
"<rootDir>/test-resources/env-vars.js"
]
},
"jest-junit": {
"outputDirectory": "reports",
"outputName": "jest-junit.xml",

File diff suppressed because it is too large Load Diff

149
backend/src/app.ts Normal file

@ -0,0 +1,149 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter,
secretApprovalRequest as v1SecretApprovalRequest
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { handleMongoInvalidDataError, requestErrorHandler } from './middleware/requestErrorHandler';
// patch async route params to handle Promise Rejections
patchRouterParam();
export const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
app.use(requestIp.mw())
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v1/secrets-approval-request', v1SecretApprovalRequest)
// v2 routes
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
// handle mongo validation errors
app.use(handleMongoInvalidDataError);
//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)
export const server = app.listen(PORT, () => {
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});

@ -1,51 +1,103 @@
import infisical from 'infisical-node';
export const getPort = () => infisical.get('PORT')! || 4000;
export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP');
export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!;
export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10;
export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!;
export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d';
export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!;
export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
export const getMongoURL = () => infisical.get('MONGO_URL')!;
export const getNodeEnv = () => infisical.get('NODE_ENV')!;
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
export const getLokiHost = () => infisical.get('LOKI_HOST')!;
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!;
export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!;
export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!;
export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!;
export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!;
export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!;
export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!;
export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!;
export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!;
export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!;
export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!;
export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!;
export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com';
export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
export const getSentryDSN = () => infisical.get('SENTRY_DSN')!;
export const getSiteURL = () => infisical.get('SITE_URL')!;
export const getSmtpHost = () => infisical.get('SMTP_HOST')!;
export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false;
export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587;
export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!;
export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!;
export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!;
export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical';
export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!;
export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!;
export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!;
export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!;
export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
const PORT = process.env.PORT || 4000;
const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400');
const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
const JWT_MFA_LIFETIME = process.env.JWT_MFA_LIFETIME! || '5m';
const JWT_MFA_SECRET = process.env.JWT_MFA_SECRET!;
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!;
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
const POSTHOG_PROJECT_API_KEY =
process.env.POSTHOG_PROJECT_API_KEY! ||
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
const SENTRY_DSN = process.env.SENTRY_DSN!;
const SITE_URL = process.env.SITE_URL!;
const SMTP_HOST = process.env.SMTP_HOST!;
const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false;
const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587;
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!;
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical';
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_TEAM = process.env.STRIPE_PRODUCT_TEAM!;
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
const LICENSE_KEY = process.env.LICENSE_KEY!;
export {
PORT,
EMAIL_TOKEN_LIFETIME,
INVITE_ONLY_SIGNUP,
ENCRYPTION_KEY,
SALT_ROUNDS,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET,
JWT_SERVICE_SECRET,
JWT_SIGNUP_LIFETIME,
JWT_SIGNUP_SECRET,
MONGO_URL,
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_AZURE,
TENANT_ID_AZURE,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB,
CLIENT_SLUG_VERCEL,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
SENTRY_DSN,
SITE_URL,
SMTP_HOST,
SMTP_PORT,
SMTP_SECURE,
SMTP_USERNAME,
SMTP_PASSWORD,
SMTP_FROM_ADDRESS,
SMTP_FROM_NAME,
STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_TEAM,
STRIPE_PRODUCT_PRO,
STRIPE_PUBLISHABLE_KEY,
STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET,
TELEMETRY_ENABLED,
LICENSE_KEY
};

@ -1,8 +1,8 @@
import * as Sentry from '@sentry/node';
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
@ -11,15 +11,15 @@ import {
ACTION_LOGIN,
ACTION_LOGOUT
} from '../../variables';
import {
NODE_ENV,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_SECRET
} from '../../config';
import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtRefreshSecret,
getJwtAuthLifetime,
getJwtAuthSecret
} from '../../config';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -126,7 +126,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: NODE_ENV === 'production' ? true : false
});
const loginAction = await EELogService.createAction({
@ -182,7 +182,7 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: NODE_ENV === 'production' ? true : false
});
const logoutAction = await EELogService.createAction({
@ -237,7 +237,7 @@ export const getNewToken = async (req: Request, res: Response) => {
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, getJwtRefreshSecret())
jwt.verify(refreshToken, JWT_REFRESH_SECRET)
);
const user = await User.findOne({
@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => {
payload: {
userId: decodedToken.userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
expiresIn: JWT_AUTH_LIFETIME,
secret: JWT_AUTH_SECRET
});
return res.status(200).send({

@ -14,6 +14,7 @@ import * as stripeController from './stripeController';
import * as userActionController from './userActionController';
import * as userController from './userController';
import * as workspaceController from './workspaceController';
import * as secretApprovalController from './secretApprovalController';
export {
authController,
@ -31,5 +32,6 @@ export {
stripeController,
userActionController,
userController,
workspaceController
workspaceController,
secretApprovalController
};

@ -2,16 +2,13 @@ import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration,
IntegrationAuth,
Bot
} from '../../models';
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import {
getApps,
getTeams,
revokeAccess
} from '../../integrations';
import { getApps, revokeAccess } from '../../integrations';
/***
* Return integration authorization with id [integrationAuthId]
@ -39,11 +36,9 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
}
export const getIntegrationOptions = async (req: Request, res: Response) => {
const INTEGRATION_OPTIONS = getIntegrationOptionsFunc();
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS,
});
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS,
});
};
/**
@ -159,54 +154,25 @@ export const saveIntegrationAccessToken = async (
* @returns
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps;
try {
const teamId = req.query.teamId as string;
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
...teamId && { teamId }
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
let apps;
try {
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({
apps
});
return res.status(200).send({
apps,
});
};
/**
* Return list of teams allowed for integration with integration authorization id [integrationAuthId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
let teams;
try {
teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization teams"
});
}
return res.status(200).send({
teams
});
}
/**
* Delete integration authorization with id [integrationAuthId]
* @param req

@ -2,7 +2,10 @@ import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration
Integration,
Workspace,
Bot,
BotKey
} from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
@ -15,7 +18,6 @@ import { eventPushSecrets } from '../../events';
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
try {
const {
integrationAuthId,
@ -32,19 +34,19 @@ export const createIntegration = async (req: Request, res: Response) => {
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets

@ -1,13 +1,13 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models';
import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models';
import {
findMembership,
deleteMembership as deleteMember
} from '../../helpers/membership';
import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { getSiteURL } from '../../config';
/**
* Check that user is a member of workspace with id [workspaceId]
@ -215,7 +215,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name,
callback_url: getSiteURL() + '/login'
callback_url: SITE_URL + '/login'
}
});
} catch (err) {

@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { createToken } from '../../helpers/auth';
@ -7,7 +8,6 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
/**
* Delete organization membership with id [membershipOrgId] from organization
@ -99,11 +99,9 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
* @returns
*/
export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg, completeInviteLink;
let invitee, inviteeMembershipOrg;
try {
const { organizationId, inviteeEmail } = req.body;
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
// validate membership
const membershipOrg = await MembershipOrg.findOne({
@ -180,13 +178,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
organizationName: organization.name,
email: inviteeEmail,
token,
callback_url: getSiteURL() + '/signupinvite'
callback_url: SITE_URL + '/signupinvite'
}
});
if (!getSmtpConfigured()) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
}
}
await updateSubscriptionOrgQuantity({ organizationId });
@ -199,8 +193,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
}
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
message: `Sent an invite link to ${req.body.inviteeEmail}`
});
};
@ -225,7 +218,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg)
throw new Error('Failed to find any invitations for email');
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
@ -257,8 +250,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
expiresIn: JWT_SIGNUP_LIFETIME,
secret: JWT_SIGNUP_SECRET
});
} catch (err) {
Sentry.setUser(null);

@ -1,18 +1,26 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
SITE_URL,
STRIPE_SECRET_KEY
} from '../../config';
import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
import {
Membership,
MembershipOrg,
Organization,
Workspace,
IncidentContactOrg
IncidentContactOrg,
IMembershipOrg
} from '../../models';
import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../../variables';
import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config';
export const getOrganizations = async (req: Request, res: Response) => {
let organizations;
@ -317,10 +325,6 @@ export const createOrganizationPortalSession = async (
) => {
let session;
try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({
customer: req.membershipOrg.organization.customerId,
@ -333,13 +337,13 @@ export const createOrganizationPortalSession = async (
customer: req.membershipOrg.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: getSiteURL() + '/dashboard',
cancel_url: getSiteURL() + '/dashboard'
success_url: SITE_URL + '/dashboard',
cancel_url: SITE_URL + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.membershipOrg.organization.customerId,
return_url: getSiteURL() + '/dashboard'
return_url: SITE_URL + '/dashboard'
});
}
@ -365,10 +369,6 @@ export const getOrganizationSubscriptions = async (
) => {
let subscriptions;
try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({
customer: req.membershipOrg.organization.customerId
});

@ -7,9 +7,9 @@ import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
/**
* Password reset step 1: Send email verification link to email [email]
@ -44,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
substitutions: {
email,
token,
callback_url: getSiteURL() + '/password-reset'
callback_url: SITE_URL + '/password-reset'
}
});
} catch (err) {
@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
expiresIn: JWT_SIGNUP_LIFETIME,
secret: JWT_SIGNUP_SECRET
});
} catch (err) {
Sentry.setUser(null);

@ -0,0 +1,320 @@
import { Request, Response } from 'express';
import SecretApprovalRequest, { ApprovalStatus, ChangeType, IApprover, IRequestedChange } from '../../models/secretApprovalRequest';
import { Builder, IBuilder } from "builder-pattern"
import { secretObjectHasRequiredFields, validateSecrets } from '../../helpers/secret';
import _ from 'lodash';
import { SECRET_PERSONAL, SECRET_SHARED } from '../../variables';
import { BadRequestError, ResourceNotFound, UnauthorizedRequestError } from '../../utils/errors';
import { ISecret, Membership, Secret, Workspace } from '../../models';
import mongoose from 'mongoose';
export const createApprovalRequest = async (req: Request, res: Response) => {
const { workspaceId, environment, requestedChanges } = req.body;
// validate workspace
const workspaceFromDB = await Workspace.findById(workspaceId)
if (!workspaceFromDB) {
throw ResourceNotFound()
}
const environmentBelongsToWorkspace = _.some(workspaceFromDB.environments, { slug: environment })
if (!environmentBelongsToWorkspace) {
throw ResourceNotFound()
}
// check for secret duplicates
const hasSecretIdDuplicates = requestedChanges.length !== _.uniqBy(requestedChanges, 'modifiedSecretParentId').length;
if (hasSecretIdDuplicates) {
throw BadRequestError({ message: "Request cannot contain changes for duplicate secrets" })
}
// ensure the workspace has approvers set
if (!workspaceFromDB.approvers.length) {
throw BadRequestError({ message: "There are no designated approvers for this project, you must set approvers first before making a request" })
}
const approverIds = _.compact(_.map(workspaceFromDB.approvers, "userId"))
const approversFormatted: IApprover[] = approverIds.map(id => {
return { "userId": id, status: ApprovalStatus.PENDING }
})
const listOfSecretIdsToModify = _.compact(_.map(requestedChanges, "modifiedSecretParentId"))
// Ensure that the user requesting changes for the set of secrets can indeed interact with said secrets
if (listOfSecretIdsToModify.length > 0) {
await validateSecrets({
userId: req.user._id.toString(),
secretIds: listOfSecretIdsToModify
});
}
const sanitizedRequestedChangesList: IRequestedChange[] = []
requestedChanges.forEach((requestedChange: IRequestedChange) => {
const secretDetailsIsValid = secretObjectHasRequiredFields(requestedChange.modifiedSecretDetails)
if (!secretDetailsIsValid) {
throw BadRequestError({ message: "One or more required fields are missing from your modified secret" })
}
if (!requestedChange.modifiedSecretParentId && (requestedChange.type != ChangeType.DELETE.toString() && requestedChange.type != ChangeType.CREATE.toString())) {
throw BadRequestError({ message: "modifiedSecretParentId can only be empty when secret change type is DELETE or CREATE" })
}
sanitizedRequestedChangesList.push(Builder<IRequestedChange>()
.modifiedSecretParentId(requestedChange.modifiedSecretParentId)
.modifiedSecretDetails(requestedChange.modifiedSecretDetails)
.approvers(approversFormatted)
.type(requestedChange.type).build())
});
const newApprovalRequest = await SecretApprovalRequest.create({
workspace: workspaceId,
requestedByUserId: req.user._id.toString(),
environment: environment,
requestedChanges: sanitizedRequestedChangesList
})
const populatedNewApprovalRequest = await newApprovalRequest.populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
return res.send({ approvalRequest: populatedNewApprovalRequest });
};
export const getAllApprovalRequestsForUser = async (req: Request, res: Response) => {
const approvalRequests = await SecretApprovalRequest.find({
requestedByUserId: req.user._id.toString()
}).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
.sort({ updatedAt: -1 })
res.send({ approvalRequests: approvalRequests })
}
export const getAllApprovalRequestsThatRequireUserApproval = async (req: Request, res: Response) => {
const approvalRequests = await SecretApprovalRequest.find({
'requestedChanges.approvers.userId': req.user._id.toString()
}).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
.sort({ updatedAt: -1 })
res.send({ approvalRequests: approvalRequests })
}
export const approveApprovalRequest = async (req: Request, res: Response) => {
const { requestedChangeIds } = req.body;
const { reviewId } = req.params
const approvalRequestFromDB = await SecretApprovalRequest.findById(reviewId)
if (!approvalRequestFromDB) {
throw ResourceNotFound()
}
const requestedChangesFromDB: IRequestedChange[] = approvalRequestFromDB.requestedChanges
const filteredChangesByIds = requestedChangesFromDB.filter(change => requestedChangeIds.includes(change._id.toString()))
if (filteredChangesByIds.length != requestedChangeIds.length) {
throw BadRequestError({ message: "All requestedChangeIds should exist in this approval request" })
}
const changesThatRequireUserApproval = _.filter(filteredChangesByIds, change => {
return _.some(change.approvers, approver => {
return approver.userId.toString() == req.user._id.toString();
});
});
if (!changesThatRequireUserApproval.length) {
throw UnauthorizedRequestError({ message: "Your approval is not required for this review" })
}
if (changesThatRequireUserApproval.length != filteredChangesByIds.length) {
throw BadRequestError({ message: "You may only request to approve changes that require your approval" })
}
changesThatRequireUserApproval.forEach((requestedChange) => {
const overallChangeStatus = requestedChange.status
const currentLoggedInUserId = req.user._id.toString()
if (overallChangeStatus == ApprovalStatus.PENDING.toString()) {
requestedChange.approvers.forEach((approver) => {
if (approver.userId.toString() == currentLoggedInUserId && approver.status == ApprovalStatus.PENDING.toString()) {
approver.status = ApprovalStatus.APPROVED
}
})
let updateOverallStatusToApproved = true
requestedChange.approvers.forEach((approver) => {
if (approver.status != ApprovalStatus.APPROVED.toString()) {
updateOverallStatusToApproved = false
}
})
if (updateOverallStatusToApproved) {
requestedChange.status = ApprovalStatus.APPROVED
}
}
})
const updatedApprovalRequest = await SecretApprovalRequest.findByIdAndUpdate(reviewId, {
requestedChanges: requestedChangesFromDB
}, { new: true }).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
res.send({ approvalRequest: updatedApprovalRequest })
}
export const rejectApprovalRequest = async (req: Request, res: Response) => {
const { requestedChangeIds } = req.body;
const { reviewId } = req.params
const approvalRequestFromDB = await SecretApprovalRequest.findById(reviewId)
if (!approvalRequestFromDB) {
throw ResourceNotFound()
}
const requestedChangesFromDB: IRequestedChange[] = approvalRequestFromDB.requestedChanges
const filteredChangesByIds = requestedChangesFromDB.filter(change => requestedChangeIds.includes(change._id.toString()))
if (filteredChangesByIds.length != requestedChangeIds.length) {
throw BadRequestError({ message: "All requestedChangeIds should exist in this approval request" })
}
const changesThatRequireUserApproval = _.filter(filteredChangesByIds, change => {
return _.some(change.approvers, approver => {
return approver.userId.toString() == req.user._id.toString();
});
});
if (!changesThatRequireUserApproval.length) {
throw UnauthorizedRequestError({ message: "Your approval is not required for this review" })
}
if (changesThatRequireUserApproval.length != filteredChangesByIds.length) {
throw BadRequestError({ message: "You may only request to reject changes that require your approval" })
}
changesThatRequireUserApproval.forEach((requestedChange) => {
const overallChangeStatus = requestedChange.status
const currentLoggedInUserId = req.user._id.toString()
if (overallChangeStatus == ApprovalStatus.PENDING.toString()) {
requestedChange.approvers.forEach((approver) => {
if (approver.userId.toString() == currentLoggedInUserId && approver.status == ApprovalStatus.PENDING.toString()) {
approver.status = ApprovalStatus.REJECTED
requestedChange.status = ApprovalStatus.REJECTED
}
})
}
})
const updatedApprovalRequest = await SecretApprovalRequest.findByIdAndUpdate(reviewId, {
requestedChanges: requestedChangesFromDB
}, { new: true }).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
res.send({ approvalRequest: updatedApprovalRequest })
};
export const mergeApprovalRequestSecrets = async (req: Request, res: Response) => {
const { requestedChangeIds } = req.body;
const { reviewId } = req.params
// only the user who requested the set of changes can merge it
const approvalRequestFromDB = await SecretApprovalRequest.findOne({ _id: reviewId, requestedByUserId: req.user._id })
if (!approvalRequestFromDB) {
throw ResourceNotFound()
}
// ensure that this user is a member of this workspace
const membershipDetails = await Membership.find({ user: req.user._id, workspace: approvalRequestFromDB.workspace })
if (!membershipDetails) {
throw UnauthorizedRequestError()
}
// filter not merged, approved, and change ids specified in this request
const filteredChangesToMerge: IRequestedChange[] = approvalRequestFromDB.requestedChanges.filter(change => change.merged == false && change.status == ApprovalStatus.APPROVED && requestedChangeIds.includes(change._id.toString()))
if (filteredChangesToMerge.length != requestedChangeIds.length) {
throw BadRequestError({ message: "One or more changes in this approval is either already merged/not approved or do not exist" })
}
const secretsToCreate: ISecret[] = []
const secretsToUpdate: any[] = []
const secretsIdsToDelete: any[] = []
const secretIdsToModify: any[] = []
filteredChangesToMerge.forEach((requestedChange: any) => {
const overallChangeStatus = requestedChange.status
const currentLoggedInUserId = req.user._id.toString()
if (overallChangeStatus == ApprovalStatus.APPROVED.toString()) {
if (ChangeType.CREATE.toString() == requestedChange.type) {
const modifiedSecret = requestedChange.modifiedSecretDetails.toObject()
secretsToCreate.push({
...modifiedSecret,
user: requestedChange.modifiedSecretDetails.type === SECRET_PERSONAL ? currentLoggedInUserId : undefined,
})
}
if (ChangeType.UPDATE.toString() == requestedChange.type) {
const modifiedSecret = requestedChange.modifiedSecretDetails.toObject()
secretIdsToModify.push(requestedChange.modifiedSecretParentId)
secretsToUpdate.push({
filter: { _id: requestedChange.modifiedSecretParentId },
update: {
$set: {
...modifiedSecret,
user: requestedChange.modifiedSecretDetails.type === SECRET_PERSONAL ? currentLoggedInUserId : undefined,
},
$inc: {
version: 1
}
}
})
}
if (ChangeType.DELETE.toString() == requestedChange.type) {
secretsIdsToDelete.push({
_id: requestedChange.modifiedSecretParentId.toString()
})
}
requestedChange.merged = true
}
})
// ensure all secrets that are to be updated exist
const numSecretsFromDBThatRequireUpdate = await Secret.countDocuments({ _id: { $in: secretIdsToModify } });
const numSecretsFromDBThatRequireDelete = await Secret.countDocuments({ _id: { $in: secretsIdsToDelete } });
if (numSecretsFromDBThatRequireUpdate != secretIdsToModify.length || numSecretsFromDBThatRequireDelete != secretsIdsToDelete.length) {
throw BadRequestError({ message: "You cannot merge changes for secrets that no longer exist" })
}
// Add add CRUD operations into a single list of operations
const allOperationsForBulkWrite: any[] = [];
for (const updateStatement of secretsToUpdate) {
allOperationsForBulkWrite.push({ updateOne: updateStatement });
}
for (const secretId of secretsIdsToDelete) {
allOperationsForBulkWrite.push({ deleteOne: { filter: { _id: secretId } } });
}
for (const createStatement of secretsToCreate) {
allOperationsForBulkWrite.push({ insertOne: { document: createStatement } });
}
// start transaction
const session = await mongoose.startSession();
session.startTransaction();
try {
await Secret.bulkWrite(allOperationsForBulkWrite);
await SecretApprovalRequest.updateOne({ _id: reviewId, 'requestedChanges._id': { $in: requestedChangeIds } },
{ $set: { 'requestedChanges.$.merged': true } })
const updatedApproval = await SecretApprovalRequest.findById(reviewId).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
res.send(updatedApproval)
} catch (error) {
await session.abortTransaction();
throw error
} finally {
session.endSession();
}
};

@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { getPostHogClient } from '../../services';
import { postHogClient } from '../../services';
interface PushSecret {
ciphertextKey: string;
@ -38,7 +38,6 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@ -112,7 +111,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
@ -181,7 +179,6 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth';
import { getJwtServiceSecret } from '../../config';
import { JWT_SERVICE_SECRET } from '../../config';
/**
* Return service token on request
@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
workspaceId
},
expiresIn: expiresIn,
secret: getJwtServiceSecret()
secret: JWT_SERVICE_SECRET
});
} catch (err) {
return res.status(400).send({

@ -1,13 +1,13 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../../models';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config';
import {
sendEmailVerification,
checkEmailVerification,
} from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
@ -21,7 +21,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
try {
email = req.body.email;
if (getInviteOnlySignup()) {
if (INVITE_ONLY_SIGNUP) {
// 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) {
@ -66,7 +66,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
const { email, code } = req.body;
// initialize user account
user = await User.findOne({ email }).select('+publicKey');
user = await User.findOne({ email });
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
@ -75,12 +75,10 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
}
// verify email
if (getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
await checkEmailVerification({
email,
code
});
if (!user) {
user = await new User({
@ -93,8 +91,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
expiresIn: JWT_SIGNUP_LIFETIME,
secret: JWT_SIGNUP_SECRET
});
} catch (err) {
Sentry.setUser(null);

@ -1,7 +1,10 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
/**
* Handle service provisioning/un-provisioning via Stripe
@ -13,15 +16,11 @@ export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
// check request for valid stripe signature
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
req.body,
sig,
getStripeWebhookSecret()
STRIPE_WEBHOOK_SECRET // ?
);
} catch (err) {
Sentry.setUser({ email: req.user.email });

@ -16,7 +16,8 @@ import {
} from "../../helpers/workspace";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
import { BadRequestError, ResourceNotFound, UnauthorizedRequestError } from "../../utils/errors";
import _ from "lodash";
/**
* Return public keys of members of workspace with id [workspaceId]
* @param req
@ -303,6 +304,112 @@ export const getWorkspaceIntegrationAuthorizations = async (
});
};
export const addApproverForWorkspaceAndEnvironment = async (
req: Request,
res: Response
) => {
interface Approver {
environment: string;
userId: string;
}
const { workspaceId } = req.params;
const { approvers }: { approvers: Approver[] } = req.body;
const workspaceFromDB = await Workspace.findById(workspaceId)
if (!workspaceFromDB) {
throw ResourceNotFound()
}
const allAvailableWorkspaceEnvironments = _.map(workspaceFromDB.environments, 'slug');
const environmentsFromApprovers = _.map(approvers, "environment")
const filteredApprovers = environmentsFromApprovers.map(environment => allAvailableWorkspaceEnvironments.includes(environment))
// validate environments
if (filteredApprovers.length != environmentsFromApprovers.length) {
const err = `One or more environments set for approver(s) is invalid`
throw BadRequestError({ message: err })
}
const approverIds = _.map(approvers, "userId")
// validate approvers membership
const approversMemberships = await Membership.find({
workspace: workspaceId,
user: { $in: approverIds }
})
if (!approversMemberships) {
throw ResourceNotFound()
}
if (approversMemberships.length != approverIds.length) {
throw UnauthorizedRequestError({ message: "Approvers must be apart of the workspace they are being added to" })
}
const updatedWorkspace = await Workspace.findByIdAndUpdate(workspaceId,
{
$addToSet: {
approvers: {
$each: approvers,
}
}
}, { new: true })
return res.json(updatedWorkspace)
};
export const removeApproverForWorkspaceAndEnvironment = async (
req: Request,
res: Response
) => {
interface Approver {
environment: string;
userId: string;
}
const { workspaceId } = req.params;
const { approvers }: { approvers: Approver[] } = req.body;
const workspaceFromDB = await Workspace.findById(workspaceId)
if (!workspaceFromDB) {
throw ResourceNotFound()
}
const allAvailableWorkspaceEnvironments = _.map(workspaceFromDB.environments, 'slug');
const environmentsFromApprovers = _.map(approvers, "environment")
const filteredApprovers = environmentsFromApprovers.map(environment => allAvailableWorkspaceEnvironments.includes(environment))
// validate environments
if (filteredApprovers.length != environmentsFromApprovers.length) {
const err = `One or more environments set for approver(s) is invalid`
throw BadRequestError({ message: err })
}
const approverIds = _.map(approvers, "userId")
// validate approvers membership
const approversMemberships = await Membership.find({
workspace: workspaceId,
user: { $in: approverIds }
})
if (!approversMemberships) {
throw ResourceNotFound()
}
if (approversMemberships.length != approverIds.length) {
throw UnauthorizedRequestError({ message: "Approvers must be apart of the workspace they are being added to" })
}
const updatedWorkspace = await Workspace.findByIdAndUpdate(workspaceId, { $pullAll: { approvers: approvers } }, { new: true })
return res.json(updatedWorkspace)
};
/**
* Return service service tokens for workspace [workspaceId] belonging to user
* @param req

@ -1,11 +1,13 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
APIKeyData
} from '../../models';
import { getSaltRounds } from '../../config';
import {
SALT_ROUNDS
} from '../../config';
/**
* Return API key data for user with id [req.user_id]
@ -43,7 +45,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);

@ -10,17 +10,17 @@ import { checkUserDevice } from '../../helpers/user';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELogService } from '../../ee/services';
import {
NODE_ENV,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET
} from '../../config';
import { BadRequestError, InternalServerError } from '../../utils/errors';
import {
TOKEN_EMAIL_MFA,
ACTION_LOGIN
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtMfaLifetime,
getJwtMfaSecret
} from '../../config';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -28,6 +28,8 @@ declare module 'jsonwebtoken' {
}
}
const clientPublicKeys: any = {};
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
@ -87,7 +89,7 @@ export const login1 = async (req: Request, res: Response) => {
*/
export const login2 = async (req: Request, res: Response) => {
try {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof } = req.body;
@ -124,15 +126,15 @@ export const login2 = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: getJwtMfaLifetime(),
secret: getJwtMfaSecret()
expiresIn: JWT_MFA_LIFETIME,
secret: JWT_MFA_SECRET
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
@ -142,13 +144,13 @@ export const login2 = async (req: Request, res: Response) => {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.ip,
@ -163,7 +165,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: NODE_ENV === 'production' ? true : false
});
// case: user does not have MFA enablgged
@ -181,7 +183,7 @@ export const login2 = async (req: Request, res: Response) => {
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
@ -191,7 +193,7 @@ export const login2 = async (req: Request, res: Response) => {
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
@ -206,14 +208,14 @@ export const login2 = async (req: Request, res: Response) => {
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(response);
}
@ -244,7 +246,7 @@ export const sendMfaToken = async (req: Request, res: Response) => {
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
@ -259,9 +261,9 @@ export const sendMfaToken = async (req: Request, res: Response) => {
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
});
}
return res.status(200).send({
message: 'Successfully sent new MFA code'
});
@ -274,87 +276,76 @@ export const sendMfaToken = async (req: Request, res: Response) => {
* @param res
*/
export const verifyMfaToken = async (req: Request, res: Response) => {
const { email, mfaToken } = req.body;
const { email, mfaToken } = req.body;
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
email,
token: mfaToken
});
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
email,
token: mfaToken
});
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false
});
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
const resObj: VerifyMfaTokenRes = {
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string
}
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
resObj.protectedKeyIV = user.protectedKeyIV;
resObj.protectedKeyTag = user.protectedKeyTag;
}
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
const resObj: VerifyMfaTokenRes = {
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string
}
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
resObj.protectedKeyIV = user.protectedKeyIV;
resObj.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(resObj);
return res.status(200).send(resObj);
}

@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { getPostHogClient } from '../../services';
import { postHogClient } from '../../services';
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
@ -15,7 +15,6 @@ import { getPostHogClient } from '../../services';
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
@ -68,7 +67,6 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@ -130,7 +128,6 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
@ -184,7 +181,6 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) {
@ -213,7 +209,6 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@ -281,7 +276,6 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@ -335,7 +329,6 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { environment } = req.query;
const { workspaceId } = req.params;

@ -15,14 +15,14 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { getPostHogClient } from '../../services';
import { postHogClient } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _ from 'lodash';
import {
BatchSecretRequest,
BatchSecretRequest,
BatchSecret
} from '../../types/secret';
@ -33,8 +33,6 @@ import {
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = getPostHogClient();
const {
workspaceId,
environment,
@ -43,13 +41,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: string;
environment: string;
requests: BatchSecretRequest[];
} = req.body;
}= req.body;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const deleteSecrets: Types.ObjectId[] = [];
const actions: IAction[] = [];
requests.forEach((request) => {
switch (request.method) {
case 'POST':
@ -72,7 +70,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
break;
}
});
// handle create secrets
let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) {
@ -111,18 +109,18 @@ export const batchSecrets = async (req: Request, res: Response) => {
});
}
}
// handle update secrets
let updatedSecrets: ISecret[] = [];
if (updateSecrets.length > 0 && req.secrets) {
// construct object containing all secrets
let listedSecretsObj: {
[key: string]: {
[key: string]: {
version: number;
type: string;
}
} = {};
listedSecretsObj = req.secrets.reduce((obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
@ -142,7 +140,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
}));
await Secret.bulkWrite(updateOperations);
const secretVersions = updateSecrets.map((u) => ({
secret: new Types.ObjectId(u._id),
version: listedSecretsObj[u._id.toString()].version,
@ -229,7 +227,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
});
}
}
if (actions.length > 0) {
// (EE) create (audit) log
await EELogService.createLog({
@ -252,7 +250,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
await EESecretService.takeSecretSnapshot({
workspaceId
});
const resObj: { [key: string]: ISecret[] | string[] } = {}
if (createSecrets.length > 0) {
@ -262,11 +260,11 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (updateSecrets.length > 0) {
resObj['updatedSecrets'] = updatedSecrets;
}
if (deleteSecrets.length > 0) {
resObj['deletedSecrets'] = deleteSecrets.map((d) => d.toString());
}
return res.status(200).send(resObj);
}
@ -328,7 +326,6 @@ export const createSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
@ -361,25 +358,9 @@ export const createSecrets = async (req: Request, res: Response) => {
tags: string[]
}
const secretsToInsert: ISecret[] = listOfSecretsToCreate.map(({
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}: secretsToCreateType) => {
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
const newlyCreatedSecrets = await Secret.insertMany(
listOfSecretsToCreate.map(({
type,
user: type === SECRET_PERSONAL ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -390,10 +371,26 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentIV,
secretCommentTag,
tags
});
})
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
}: secretsToCreateType) => {
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
user: type === SECRET_PERSONAL ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
});
})
);
setTimeout(async () => {
// trigger event - push secrets
@ -533,7 +530,6 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const postHogClient = getPostHogClient();
const { workspaceId, environment, tagSlugs } = req.query;
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
@ -736,7 +732,6 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
@ -958,7 +953,6 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);

@ -1,13 +1,15 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceTokenData
} from '../../models';
import {
SALT_ROUNDS
} from '../../config';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { ABILITY_READ } from '../../variables/organization';
import { getSaltRounds } from '../../config';
/**
* Return service token data associated with service token on request
@ -15,35 +17,7 @@ import { getSaltRounds } from '../../config';
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"serviceTokenData": {
"type": "object",
$ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token"
}
}
}
}
}
}
*/
return res.status(200).json(req.serviceTokenData);
}
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
/**
* Create new service token data for workspace with id [workspaceId] and
@ -54,7 +28,6 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;
try {
const {
name,
@ -63,8 +36,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
encryptedKey,
iv,
tag,
expiresIn,
permissions
expiresIn
} = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
@ -73,7 +45,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
}
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
@ -87,8 +59,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
secretHash,
encryptedKey,
iv,
tag,
permissions
tag
}).save();
// return service token data without sensitive data
@ -140,4 +111,4 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}
}

@ -7,8 +7,8 @@ import {
} from '../../helpers/signup';
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import { NODE_ENV } from '../../config';
import request from '../../config/request';
import { getNodeEnv, getLoopsApiKey } from '../../config';
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -108,7 +108,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
token = tokens.token;
// sending a welcome email to new users
if (getLoopsApiKey()) {
if (process.env.LOOPS_API_KEY) {
await request.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
@ -117,7 +117,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + getLoopsApiKey()
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
},
});
}
@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: NODE_ENV === 'production' ? true : false
});
} catch (err) {
Sentry.setUser(null);
@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: NODE_ENV === 'production' ? true : false
});
} catch (err) {
Sentry.setUser(null);

@ -41,7 +41,7 @@ export const getMe = async (req: Request, res: Response) => {
try {
user = await User
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
.select('+publicKey +encryptedPrivateKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);

@ -19,7 +19,7 @@ import {
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services';
import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface V2PushSecret {
@ -48,7 +48,6 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@ -122,7 +121,6 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;

@ -1,86 +0,0 @@
import AWS from 'aws-sdk'
export const createTemporaryIAMUser = async (rootAccessKeyId, rootSecretAccessKey, region, userName, policyDocument, durationInSeconds) => {
// Configure AWS SDK with your root user credentials
AWS.config.update({
accessKeyId: rootAccessKeyId,
secretAccessKey: rootSecretAccessKey,
region: region,
});
const iam = new AWS.IAM();
const sts = new AWS.STS();
// Get the account ID
const callerIdentity = await sts.getCallerIdentity().promise();
const accountId = callerIdentity.Account;
// Create the IAM role
const roleName = `Role-${userName}`;
const assumeRolePolicyDocument = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
AWS: `arn:aws:iam::${accountId}:root`,
},
Action: 'sts:AssumeRole',
},
],
};
const createRoleParams = {
RoleName: roleName,
AssumeRolePolicyDocument: JSON.stringify(assumeRolePolicyDocument),
};
const role = await iam.createRole(createRoleParams).promise();
// Create and attach the policy to the IAM role
const policyName = `Policy-${userName}`;
const createPolicyParams = {
PolicyName: policyName,
PolicyDocument: JSON.stringify(policyDocument),
};
const policy = await iam.createPolicy(createPolicyParams).promise();
const attachRolePolicyParams = {
PolicyArn: policy.Policy.Arn,
RoleName: roleName,
};
await iam.attachRolePolicy(attachRolePolicyParams).promise();
// Create temporary credentials for the IAM role
const assumeRoleParams = {
RoleArn: role.Role.Arn,
RoleSessionName: `TemporarySession-${userName}`,
DurationSeconds: durationInSeconds,
};
const credentials = await sts.assumeRole(assumeRoleParams).promise();
// Return the temporary credentials
return {
accessKeyId: credentials.Credentials.AccessKeyId,
secretAccessKey: credentials.Credentials.SecretAccessKey,
sessionToken: credentials.Credentials.SessionToken,
};
};
// module.exports = createTemporaryIAMUser;
// // Example policy document
// const policyDocument = {
// Version: '2012-10-17',
// Statement: [
// {
// Action: 's3:ListBucket',
// Effect: 'Allow',
// Resource: 'arn:aws:s3:::example-bucket',
// },
// ],
// };

@ -1,7 +1,10 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
/**
* Handle service provisioning/un-provisioning via Stripe
@ -12,16 +15,12 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
req.body,
sig,
getStripeWebhookSecret()
STRIPE_WEBHOOK_SECRET // ?
);
} catch (err) {
Sentry.setUser({ email: req.user.email });

@ -1,3 +1,5 @@
import { LICENSE_KEY } from '../../config';
/**
* Class to handle Enterprise Edition license actions
*/
@ -14,4 +16,4 @@ class EELicenseService {
}
}
export default new EELicenseService('N/A');
export default new EELicenseService(LICENSE_KEY);

@ -1,12 +1,17 @@
import * as Sentry from '@sentry/node';
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import {
IUser,
User,
ServiceTokenData,
APIKeyData
} from '../models';
import {
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET
} from '../config';
import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
@ -14,12 +19,6 @@ import {
UnauthorizedRequestError,
BadRequestError
} from '../utils/errors';
import {
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
/**
*
@ -93,7 +92,7 @@ const getAuthUserPayload = async ({
let user;
try {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
);
user = await User.findOne({
@ -149,10 +148,7 @@ const getAuthSTDPayload = async ({
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
.select('+encryptedKey +iv +tag').populate('user');
} catch (err) {
throw UnauthorizedRequestError({
@ -224,16 +220,16 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
expiresIn: JWT_AUTH_LIFETIME,
secret: JWT_AUTH_SECRET
});
refreshToken = createToken({
payload: {
userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
expiresIn: JWT_REFRESH_LIFETIME,
secret: JWT_REFRESH_SECRET
});
} catch (err) {
Sentry.setUser(null);

@ -12,8 +12,8 @@ import {
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables';
import { getEncryptionKey } from '../config';
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@ -33,7 +33,7 @@ const createBot = async ({
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: getEncryptionKey()
key: ENCRYPTION_KEY
});
bot = await new Bot({
@ -130,7 +130,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: getEncryptionKey()
key: ENCRYPTION_KEY
});
key = decryptAsymmetric({

@ -29,23 +29,6 @@ const initDatabaseHelper = async ({
return mongoose.connection;
}
/**
* Close database conection
*/
const closeDatabaseHelper = async () => {
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
mongoose.connection.close()
.then(() => resolve('Database connection closed'));
} else {
resolve('Database connection already closed');
}
})
]);
}
export {
initDatabaseHelper,
closeDatabaseHelper
initDatabaseHelper
}

@ -229,7 +229,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({
integrationAuth,
integration: integrationAuth.integration,
refreshToken
});
}

@ -1,9 +1,9 @@
import * as Sentry from '@sentry/node';
import fs from 'fs';
import path from 'path';
import handlebars from 'handlebars';
import nodemailer from 'nodemailer';
import { getSmtpFromName, getSmtpFromAddress, getSmtpConfigured } from '../config';
import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
import * as Sentry from '@sentry/node';
let smtpTransporter: nodemailer.Transporter;
@ -25,25 +25,23 @@ const sendMail = async ({
recipients: string[];
substitutions: any;
}) => {
if (getSmtpConfigured()) {
try {
const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template),
'utf8'
);
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
try {
const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template),
'utf8'
);
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({
from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
await smtpTransporter.sendMail({
from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
};

@ -1,14 +1,23 @@
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import {
STRIPE_SECRET_KEY,
STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_TEAM,
STRIPE_PRODUCT_PRO
} from '../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
import { Types } from 'mongoose';
import { ACCEPTED } from '../variables';
import { Organization, MembershipOrg } from '../models';
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter
} from '../config';
const productToPriceMap = {
starter: STRIPE_PRODUCT_STARTER,
team: STRIPE_PRODUCT_TEAM,
pro: STRIPE_PRODUCT_PRO
};
/**
* Create an organization with name [name]
@ -27,11 +36,8 @@ const createOrganization = async ({
let organization;
try {
// register stripe account
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
if (getStripeSecretKey()) {
if (STRIPE_SECRET_KEY) {
const customer = await stripe.customers.create({
email,
description: name
@ -81,16 +87,6 @@ const initSubscriptionOrg = async ({
if (organization) {
if (organization.customerId) {
// initialize starter subscription with quantity of 0
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const productToPriceMap = {
starter: getStripeProductStarter(),
team: getStripeProductTeam(),
pro: getStripeProductPro()
};
stripeSubscription = await stripe.subscriptions.create({
customer: organization.customerId,
items: [
@ -143,10 +139,6 @@ const updateSubscriptionOrgQuantity = async ({
status: ACCEPTED
});
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const subscription = (
await stripe.subscriptions.list({
customer: organization.customerId
@ -175,4 +167,4 @@ export {
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};
};

@ -267,7 +267,7 @@ const v1PushSecrets = async ({
if (toAdd.length > 0) {
// add secrets
const newSecrets: ISecret[] = (await Secret.insertMany(
const newSecrets = await Secret.insertMany(
toAdd.map((s, idx) => {
const obj: any = {
version: 1,
@ -294,7 +294,7 @@ const v1PushSecrets = async ({
return obj;
})
)).map((insertedSecret) => insertedSecret.toObject());
);
// (EE) add secret versions for new secrets
EESecretService.addSecretVersions({
@ -713,10 +713,27 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
return reformatedSecrets;
};
const secretObjectHasRequiredFields = (secretObject: ISecret) => {
if (!secretObject.type ||
!(secretObject.type === SECRET_PERSONAL || secretObject.type === SECRET_SHARED) ||
!secretObject.secretKeyCiphertext ||
!secretObject.secretKeyIV ||
!secretObject.secretKeyTag ||
(typeof secretObject.secretValueCiphertext !== 'string') ||
!secretObject.secretValueIV ||
!secretObject.secretValueTag) {
return false
}
return true
}
export {
validateSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,
reformatPullSecrets
reformatPullSecrets,
secretObjectHasRequiredFields
};

@ -9,8 +9,10 @@ import {
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from '../variables';
import {
SALT_ROUNDS
} from '../config';
import { UnauthorizedRequestError } from '../utils/errors';
import { getSaltRounds } from '../config';
/**
* Create and store a token in the database for purpose [type]
@ -84,7 +86,7 @@ const createTokenHelper = async ({
const query: TokenDataQuery = { type };
const update: TokenDataUpdate = {
type,
tokenHash: await bcrypt.hash(token, getSaltRounds()),
tokenHash: await bcrypt.hash(token, SALT_ROUNDS),
expiresAt
}

@ -1,183 +1,28 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
import infisical from 'infisical-node';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import * as Sentry from '@sentry/node';
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
import { server } from './app';
import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { logTelemetryMessage } from './services';
import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import cookieParser from 'cookie-parser';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
import {
getMongoURL,
getNodeEnv,
getPort,
getSentryDSN,
getSiteURL
} from './config';
DatabaseService.initDatabase(MONGO_URL);
const main = async () => {
if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) {
await infisical.connect({
token: process.env.INFISICAL_TOKEN!
});
}
setUpHealthEndpoint(server);
logTelemetryMessage();
setTransporter(initSmtp());
setTransporter(initSmtp());
await DatabaseService.initDatabase(getMongoURL());
if (getNodeEnv() !== 'test') {
Sentry.init({
dsn: getSentryDSN(),
tracesSampleRate: 1.0,
debug: getNodeEnv() === 'production' ? false : true,
environment: getNodeEnv()
});
}
patchRouterParam();
const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: getSiteURL()
})
);
app.use(requestIp.mw());
if (getNodeEnv() === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
app.use(requestErrorHandler)
const server = app.listen(getPort(), () => {
getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
});
createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {
await DatabaseService.closeDatabase();
})
return server;
if (NODE_ENV !== 'test') {
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
}
export default main();
createTestUserForDevelopment()

@ -10,13 +10,11 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
@ -25,30 +23,26 @@ import {
INTEGRATION_TRAVISCI_API_URL,
} from "../variables";
interface App {
name: string;
appId?: string;
owner?: string;
}
/**
* Return list of names of apps for integration named [integration]
* @param {Object} obj
* @param {String} obj.integration - name of integration
* @param {String} obj.accessToken - access token for integration
* @param {String} obj.teamId - (optional) id of team for getting integration apps (used for integrations like GitLab)
* @returns {Object[]} apps - names of integration apps
* @returns {String} apps.name - name of integration app
*/
const getApps = async ({
integrationAuth,
accessToken,
teamId
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
teamId?: string;
}) => {
interface App {
name: string;
appId?: string;
owner?: string;
}
let apps: App[] = [];
try {
@ -83,12 +77,6 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_GITLAB:
apps = await getAppsGitlab({
accessToken,
teamId
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken,
@ -202,40 +190,21 @@ const getAppsVercel = async ({
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
const apps: any = [];
let apps;
try {
let page = 1;
const perPage = 10;
let hasMorePages = true;
// paginate through all sites
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
params,
const res = (
await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.site_id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
})
).data;
page++;
}
apps = res.map((a: any) => ({
name: a.name,
appId: a.site_id,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -248,9 +217,9 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
/**
* Return list of repositories for Github integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Github API
* @returns {Object[]} apps - names of Github sites
* @returns {String} apps.name - name of Github site
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
let apps;
@ -432,117 +401,4 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
return apps;
}
/**
* Return list of repositories for GitLab integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for GitLab API
* @returns {Object[]} apps - names of GitLab sites
* @returns {String} apps.name - name of GitLab site
*/
const getAppsGitlab = async ({
accessToken,
teamId
}: {
accessToken: string;
teamId?: string;
}) => {
const apps: App[] = [];
let page = 1;
const perPage = 10;
let hasMorePages = true;
try {
if (teamId) {
// case: fetch projects for group with id [teamId] in GitLab
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
} else {
// case: fetch projects for individual in GitLab
const { id } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab projects");
}
return apps;
}
export { getApps };

@ -1,32 +1,28 @@
import * as Sentry from '@sentry/node';
import request from '../config/request';
import * as Sentry from '@sentry/node';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL
INTEGRATION_GITHUB_TOKEN_URL
} from '../variables';
import {
getSiteURL,
getClientIdAzure,
getClientSecretAzure,
getClientSecretHeroku,
getClientIdVercel,
getClientSecretVercel,
getClientIdNetlify,
getClientSecretNetlify,
getClientIdGitHub,
getClientSecretGitHub,
getClientIdGitLab,
getClientSecretGitLab
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
interface ExchangeCodeAzureResponse {
@ -70,15 +66,6 @@ interface ExchangeCodeGithubResponse {
token_type: string;
}
interface ExchangeCodeGitlabResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scope: string;
created_at: number;
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration]
@ -127,10 +114,6 @@ const exchangeCode = async ({
code
});
break;
case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({
code
});
}
} catch (err) {
Sentry.setUser(null);
@ -159,16 +142,16 @@ const exchangeCodeAzure = async ({
grant_type: 'authorization_code',
code: code,
scope: 'https://vault.azure.net/.default openid offline_access',
client_id: getClientIdAzure(),
client_secret: getClientSecretAzure(),
redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
client_id: CLIENT_ID_AZURE,
client_secret: CLIENT_SECRET_AZURE,
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
} catch (err: any) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
@ -204,7 +187,7 @@ const exchangeCodeHeroku = async ({
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_secret: getClientSecretHeroku()
client_secret: CLIENT_SECRET_HEROKU
} as any)
)).data;
@ -242,9 +225,9 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: getClientIdVercel(),
client_secret: getClientSecretVercel(),
redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback`
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
@ -282,9 +265,9 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: getClientIdNetlify(),
client_secret: getClientSecretNetlify(),
redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback`
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
@ -333,10 +316,10 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
res = (
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: getClientIdGitHub(),
client_secret: getClientSecretGitHub(),
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback`
redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
'Accept': 'application/json',
@ -358,53 +341,4 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Gitlab
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for Gitlab API
* @returns {String} obj2.refreshToken - refresh token for Gitlab API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
let res: ExchangeCodeGitlabResponse;
const accessExpiresAt = new Date();
try {
res = (
await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: getClientIdGitLab(),
client_secret: getClientSecretGitLab(),
redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any),
{
headers: {
"Accept-Encoding": "application/json",
}
}
)
).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
}
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
};
}
export { exchangeCode };

@ -1,7 +1,6 @@
import { exchangeCode } from './exchange';
import { exchangeRefresh } from './refresh';
import { getApps } from './apps';
import { getTeams } from './teams';
import { syncSecrets } from './sync';
import { revokeAccess } from './revoke';
@ -9,7 +8,6 @@ export {
exchangeCode,
exchangeRefresh,
getApps,
getTeams,
syncSecrets,
revokeAccess
}

@ -1,29 +1,16 @@
import * as Sentry from '@sentry/node';
import request from '../config/request';
import * as Sentry from '@sentry/node';
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_GITLAB,
} from '../variables';
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU
} from '../config';
import {
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL
INTEGRATION_HEROKU_TOKEN_URL
} from '../variables';
import {
IntegrationService
} from '../services';
import {
getSiteURL,
getClientIdAzure,
getClientSecretAzure,
getClientSecretHeroku,
getClientIdGitLab,
getClientSecretGitLab
} from '../config';
interface RefreshTokenAzureResponse {
token_type: string;
@ -34,23 +21,6 @@ interface RefreshTokenAzureResponse {
refresh_token: string;
}
interface RefreshTokenHerokuResponse {
access_token: string;
expires_in: number;
refresh_token: string;
token_type: string;
user_id: string;
}
interface RefreshTokenGitLabResponse {
token_type: string;
scope: string;
expires_in: number;
access_token: string;
refresh_token: string;
created_at: number;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@ -59,61 +29,33 @@ interface RefreshTokenGitLabResponse {
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
*/
const exchangeRefresh = async ({
integrationAuth,
integration,
refreshToken
}: {
integrationAuth: IIntegrationAuth;
integration: string;
refreshToken: string;
}) => {
interface TokenDetails {
accessToken: string;
refreshToken: string;
accessExpiresAt: Date;
}
let tokenDetails: TokenDetails;
let accessToken;
try {
switch (integrationAuth.integration) {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
tokenDetails = await exchangeRefreshAzure({
accessToken = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU:
tokenDetails = await exchangeRefreshHeroku({
accessToken = await exchangeRefreshHeroku({
refreshToken
});
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken
});
break;
default:
throw new Error('Failed to exchange token for incompatible integration');
}
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt
});
await IntegrationService.setIntegrationAuthRefresh({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: tokenDetails.refreshToken
});
}
return tokenDetails.accessToken;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
}
return accessToken;
};
/**
@ -129,27 +71,18 @@ const exchangeRefreshAzure = async ({
refreshToken: string;
}) => {
try {
const accessExpiresAt = new Date();
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
const res: RefreshTokenAzureResponse = (await request.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: getClientIdAzure(),
client_id: CLIENT_ID_AZURE,
scope: 'openid offline_access',
refresh_token: refreshToken,
grant_type: 'refresh_token',
client_secret: getClientSecretAzure()
client_secret: CLIENT_SECRET_AZURE
} as any)
);
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
return res.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -169,84 +102,26 @@ const exchangeRefreshHeroku = async ({
}: {
refreshToken: string;
}) => {
let accessToken;
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenHerokuResponse
} = await request.post(
const res = await request.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: getClientSecretHeroku()
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* GitLab integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for GitLab
* @returns
*/
const exchangeRefreshGitLab = async ({
refreshToken
}: {
refreshToken: string;
}) => {
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenGitLabResponse
} = await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: getClientIdGitLab,
client_secret: getClientSecretGitLab(),
redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any),
{
headers: {
"Accept-Encoding": "application/json",
}
});
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for GitLab');
}
return accessToken;
};
export { exchangeRefresh };

@ -10,8 +10,7 @@ import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_GITHUB
} from '../variables';
const revokeAccess = async ({
@ -33,8 +32,6 @@ const revokeAccess = async ({
break;
case INTEGRATION_GITHUB:
break;
case INTEGRATION_GITLAB:
break;
}
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({

@ -19,13 +19,11 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
@ -112,13 +110,6 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_GITLAB:
await syncSecretsGitLab({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_RENDER:
await syncSecretsRender({
integration,
@ -172,6 +163,7 @@ const syncSecretsAzureKeyVault = async ({
accessToken: string;
}) => {
try {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
attributes: {
@ -194,22 +186,17 @@ const syncSecretsAzureKeyVault = async ({
*/
const paginateAzureKeyVaultSecrets = async (url: string) => {
let result: GetAzureKeyVaultSecret[] = [];
try {
while (url) {
const res = await request.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
result = result.concat(res.data.value);
url = res.data.nextLink;
}
while (url) {
const res = await request.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
result = result.concat(res.data.value);
url = res.data.nextLink;
}
return result;
@ -225,7 +212,8 @@ const syncSecretsAzureKeyVault = async ({
const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
'Authorization': `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
@ -271,75 +259,33 @@ const syncSecretsAzureKeyVault = async ({
deleteSecrets.push(res[key]);
}
});
const setSecretAzureKeyVault = async ({
key,
value,
integration,
accessToken
}: {
key: string;
value: string;
integration: IIntegration;
accessToken: string;
}) => {
let isSecretSet = false;
let maxTries = 6;
while (!isSecretSet && maxTries > 0) {
// try to set secret
try {
await request.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value
},
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
isSecretSet = true;
} catch (err) {
const error: any = err;
if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') {
await request.post(
`${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {},
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
await new Promise(resolve => setTimeout(resolve, 10000));
} else {
await new Promise(resolve => setTimeout(resolve, 10000));
maxTries--;
}
}
}
}
// Sync/push set secrets
for await (const setSecret of setSecrets) {
const { key, value } = setSecret;
setSecretAzureKeyVault({
key,
value,
integration,
accessToken
if (setSecrets.length > 0) {
setSecrets.forEach(async ({ key, value }) => {
await request.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
});
}
for await (const deleteSecret of deleteSecrets) {
const { key } = deleteSecret;
await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret) => {
await request.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
});
}
} catch (err) {
@ -1476,98 +1422,7 @@ const syncSecretsTravisCI = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to GitLab");
}
}
/**
* Sync/push [secrets] to GitLab repo with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for GitLab integration
*/
const syncSecretsGitLab = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
// get secrets from gitlab
const getSecretsRes = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {
await request.post(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
key: key,
value: secrets[key],
protected: false,
masked: false,
raw: false,
environment_scope:'*'
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
)
} else {
// udpate secret
await request.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`,
{
...existingSecret,
value: secrets[existingSecret.key]
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
)
}
}
// delete secrets
for await (const sec of getSecretsRes) {
if (!(sec.key in secrets)) {
await request.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
},
}
);
}
}
}catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to GitLab");
throw new Error("Failed to sync secrets to TravisCI");
}
}

@ -1,92 +0,0 @@
import * as Sentry from "@sentry/node";
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_GITLAB,
INTEGRATION_GITLAB_API_URL
} from '../variables';
import request from '../config/request';
interface Team {
name: string;
teamId: string;
}
/**
* Return list of teams for integration authorization [integrationAuth]
* @param {Object} obj
* @param {String} obj.integrationAuth - integration authorization to get teams for
* @param {String} obj.accessToken - access token for integration authorization
* @returns {Object[]} teams - teams of integration authorization
* @returns {String} teams.name - name of team
* @returns {String} teams.teamId - id of team
*/
const getTeams = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let teams: Team[] = [];
try {
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration teams');
}
return teams;
}
/**
* Return list of teams for GitLab integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for GitLab API
* @returns {Object[]} teams - teams that user is part of in GitLab
* @returns {String} teams.name - name of team
* @returns {String} teams.teamId - id of team
*/
const getTeamsGitLab = async ({
accessToken
}: {
accessToken: string;
}) => {
let teams: Team[] = [];
try {
const res = (await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
)).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab integration teams");
}
return teams;
}
export {
getTeams
}

@ -1,13 +1,16 @@
import * as Sentry from '@sentry/node';
import { ErrorRequestHandler } from "express";
import { InternalServerError } from "../utils/errors";
import * as Sentry from '@sentry/node';
import { InternalServerError, UnauthorizedRequestError, UnprocessableEntityError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
import { getNodeEnv } from '../config';
import { NODE_ENV } from "../config";
import mongoose from "mongoose";
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
if (res.headersSent) return next();
if (getNodeEnv() !== "production") {
if (NODE_ENV !== "production") {
/* eslint-disable no-console */
console.log(error)
/* eslint-enable no-console */
@ -31,4 +34,17 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}
export const handleMongoInvalidDataError = (err: any, req: any, res: any, next: any) => {
if (err instanceof mongoose.Error.ValidationError) {
const errors: any = {};
for (const field in err.errors) {
errors[field] = err.errors[field].message;
}
throw UnprocessableEntityError({ message: JSON.stringify(errors) })
} else {
next(err);
}
}

@ -1,14 +1,12 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User, ServiceTokenData } from '../models';
import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -27,11 +25,9 @@ declare module 'jsonwebtoken' {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = ['jwt'],
requiredServiceTokenPermissions = []
acceptedAuthModes = ['jwt']
}: {
acceptedAuthModes: string[];
requiredServiceTokenPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
@ -42,22 +38,11 @@ const requireAuth = ({
});
// attach auth payloads
let serviceTokenData: any;
switch (authTokenType) {
case 'serviceToken':
serviceTokenData = await getAuthSTDPayload({
req.serviceTokenData = await getAuthSTDPayload({
authTokenValue
});
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({

@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User } from '../models';
import { JWT_MFA_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { getJwtMfaSecret } from '../config';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -26,7 +26,7 @@ const requireMfaAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, getJwtMfaSecret())
jwt.verify(AUTH_TOKEN_VALUE, JWT_MFA_SECRET)
);
const user = await User.findOne({

@ -23,7 +23,7 @@ const requireSecretsAuth = ({
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secrets.id]
secretIds: req.body.secrets.id
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({

@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { ServiceToken } from '../models';
import { JWT_SERVICE_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { getJwtServiceSecret } from '../config';
// TODO: deprecate
declare module 'jsonwebtoken' {
@ -33,7 +33,7 @@ const requireServiceTokenAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, getJwtServiceSecret())
jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET)
);
const serviceToken = await ServiceToken.findOne({

@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User } from '../models';
import { JWT_SIGNUP_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { getJwtSignupSecret } from '../config';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -27,7 +27,7 @@ const requireSignupAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, getJwtSignupSecret())
jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET)
);
const user = await User.findOne({

@ -21,7 +21,7 @@ const requireWorkspaceAuth = ({
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { workspaceId } = req[location];
if (req.user) {
// case: jwt auth
const membership = await validateMembership({
@ -32,11 +32,11 @@ const requireWorkspaceAuth = ({
req.membership = membership;
}
if (
req.serviceTokenData
&& req.serviceTokenData.workspace.toString() !== workspaceId
&& req.serviceTokenData.environment !== req.body.environment
&& req.serviceTokenData.workspace !== workspaceId
&& req.serviceTokenData.environment !== req.query.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}

@ -7,7 +7,6 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
@ -32,8 +31,7 @@ export interface IIntegration {
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'gitlab'
| 'github'
| 'render'
| 'flyio'
| 'circleci'
@ -62,11 +60,13 @@ const integrationSchema = new Schema<IIntegration>(
default: null,
},
appId: {
// (new)
// id of app in provider
type: String,
default: null,
},
targetEnvironment: {
// (new)
// target environment
type: String,
default: null,
@ -96,7 +96,6 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,

@ -7,7 +7,6 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
@ -17,15 +16,15 @@ import {
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
teamId: string;
accountId: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string;
accessIdIV?: string;
accessIdTag?: string;
accessIdCiphertext?: string; // new
accessIdIV?: string; // new
accessIdTag?: string; // new
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
@ -49,7 +48,6 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,

@ -26,7 +26,7 @@ export interface ISecret {
tags?: string[];
}
const secretSchema = new Schema<ISecret>(
export const secretSchema = new Schema<ISecret>(
{
version: {
type: Number,

@ -1,18 +1,28 @@
import mongoose, { Schema, model } from 'mongoose';
import Secret, { ISecret } from './secret';
import Secret, { ISecret, secretSchema } from './secret';
export interface IRequestedChange {
_id: string
userId: mongoose.Types.ObjectId;
status: ApprovalStatus;
modifiedSecretDetails: ISecret,
modifiedSecretParentId: mongoose.Types.ObjectId,
type: string,
approvers: IApprover[]
merged: boolean
}
interface ISecretApprovalRequest {
secret: mongoose.Types.ObjectId;
requestedChanges: ISecret;
requestedBy: mongoose.Types.ObjectId;
approvers: IApprover[];
status: ApprovalStatus;
environment: string;
workspace: mongoose.Types.ObjectId;
requestedChanges: IRequestedChange[];
requestedByUserId: mongoose.Types.ObjectId;
timestamp: Date;
requestType: RequestType;
requestType: ChangeType;
requestId: string;
}
interface IApprover {
export interface IApprover {
userId: mongoose.Types.ObjectId;
status: ApprovalStatus;
}
@ -23,54 +33,80 @@ export enum ApprovalStatus {
REJECTED = 'rejected'
}
export enum RequestType {
export enum ChangeType {
UPDATE = 'update',
DELETE = 'delete',
CREATE = 'create'
}
const approverSchema = new mongoose.Schema({
user: {
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
required: false,
},
status: {
type: String,
enum: [ApprovalStatus],
default: ApprovalStatus.PENDING
}
}, { timestamps: true });
// extend the Secret Schema by taking all but removing _id and version fields
const SecretModificationSchema = new Schema({
...secretSchema.obj,
}, {
_id: false,
});
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
SecretModificationSchema.remove("version")
const requestedChangeSchema = new mongoose.Schema(
{
secret: {
_id: { type: mongoose.Schema.Types.ObjectId, auto: true },
modifiedSecretDetails: SecretModificationSchema,
modifiedSecretParentId: { // used to fetch the current version of this secret for comparing
type: mongoose.Schema.Types.ObjectId,
ref: 'Secret'
},
requestedChanges: Secret,
requestedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
type: {
type: String,
enum: ChangeType,
required: true
},
approvers: [approverSchema],
status: {
type: String,
enum: ApprovalStatus,
default: ApprovalStatus.PENDING
default: ApprovalStatus.PENDING // the overall status of the requested change
},
timestamp: {
type: Date,
default: Date.now
approvers: [approverSchema],
merged: {
type: Boolean,
default: false,
}
},
{ timestamps: true }
);
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
environment: {
type: String, // The secret changes were requested for
ref: 'Secret'
},
requestType: {
type: String,
enum: RequestType,
required: true
workspace: {
type: mongoose.Schema.Types.ObjectId, // workspace id of the secret
ref: 'Workspace'
},
requestedChanges: [requestedChangeSchema], // the changes that the requested user wants to make to the existing secret
requestedByUserId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
requestId: {
type: String,
required: false
}
},
{
@ -78,6 +114,8 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
}
);
const SecretApprovalRequest = model<ISecretApprovalRequest>('SecretApprovalRequest', secretApprovalRequestSchema);
secretApprovalRequestSchema.index({ 'requestedChanges.approvers.userId': 1 });
const SecretApprovalRequest = model<ISecretApprovalRequest>('secret_approval_request', secretApprovalRequestSchema);
export default SecretApprovalRequest;

@ -3,14 +3,13 @@ import { Schema, model, Types } from 'mongoose';
export interface IServiceTokenData {
name: string;
workspace: Types.ObjectId;
environment: string;
environment: string; // TODO: adapt to upcoming environment id
user: Types.ObjectId;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
permissions: string[];
}
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
@ -52,11 +51,6 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
tag: {
type: String,
select: false
},
permissions: {
type: [String],
enum: ['read', 'write'],
default: ['read']
}
},
{

@ -1,4 +1,5 @@
import { Schema, model } from 'mongoose';
import { EMAIL_TOKEN_LIFETIME } from '../config';
export interface IToken {
email: string;

@ -1,9 +1,16 @@
import { Schema, model, Types } from 'mongoose';
import mongoose, { Schema, model, Types } from 'mongoose';
export interface DesignatedApprovers {
environment: string,
approvers: [mongoose.Schema.Types.ObjectId]
}
export interface IWorkspace {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
approvers: [DesignatedApprovers];
environments: Array<{
name: string;
slug: string;
@ -11,6 +18,16 @@ export interface IWorkspace {
autoCapitalization: boolean;
}
const approverSchema = new mongoose.Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
},
environment: {
type: String
}
}, { _id: false });
const workspaceSchema = new Schema<IWorkspace>({
name: {
type: String,
@ -20,6 +37,7 @@ const workspaceSchema = new Schema<IWorkspace>({
type: Boolean,
default: true,
},
approvers: [approverSchema],
organization: {
type: Schema.Types.ObjectId,
ref: 'Organization',

@ -1,5 +1,4 @@
import express, { Request, Response } from 'express';
import { getSmtpConfigured } from '../../config';
const router = express.Router();
@ -9,7 +8,6 @@ router.get(
res.status(200).json({
date: new Date(),
message: 'Ok',
emailConfigured: getSmtpConfigured()
})
}
);

@ -15,6 +15,7 @@ import password from './password';
import stripe from './stripe';
import integration from './integration';
import integrationAuth from './integrationAuth';
import secretApprovalRequest from './secretApprovalsRequest'
export {
signup,
@ -33,5 +34,6 @@ export {
password,
stripe,
integration,
integrationAuth
integrationAuth,
secretApprovalRequest
};

@ -1,6 +1,6 @@
import express from 'express';
const router = express.Router();
import { body, param, query } from 'express-validator';
import { body, param } from 'express-validator';
import {
requireAuth,
requireWorkspaceAuth,
@ -73,24 +73,10 @@ router.get(
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
query('teamId'),
validateRequest,
integrationAuthController.getIntegrationAuthApps
);
router.get(
'/:integrationAuthId/teams',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
validateRequest,
integrationAuthController.getIntegrationAuthTeams
);
router.delete(
'/:integrationAuthId',
requireAuth({

@ -0,0 +1,65 @@
import express from 'express';
const router = express.Router();
import { requireAuth, validateRequest } from '../../middleware';
import { secretApprovalController } from '../../controllers/v1';
import { body, param } from 'express-validator';
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('workspaceId').exists(),
body('environment').exists(),
body('requestedChanges').isArray(),
validateRequest,
secretApprovalController.createApprovalRequest
);
router.get(
'/sent',
requireAuth({
acceptedAuthModes: ['jwt']
}),
secretApprovalController.getAllApprovalRequestsForUser
);
router.get(
'/approvals-needed',
requireAuth({
acceptedAuthModes: ['jwt']
}),
secretApprovalController.getAllApprovalRequestsThatRequireUserApproval
);
router.post(
'/:reviewId/approve',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('requestedChangeIds').isArray(),
validateRequest,
secretApprovalController.approveApprovalRequest
);
router.post(
'/:reviewId/reject',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('requestedChangeIds').isArray(),
validateRequest,
secretApprovalController.rejectApprovalRequest
);
router.post(
'/:reviewId/merge',
body('requestedChangeIds').isArray(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
}),
secretApprovalController.mergeApprovalRequestSecrets
);
export default router;

@ -36,10 +36,10 @@ router.get(
);
router.get(
'/',
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
}),
workspaceController.getWorkspaces
);
@ -134,6 +134,34 @@ router.get(
workspaceController.getWorkspaceIntegrationAuthorizations
);
router.post(
'/:workspaceId/approvers',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN]
}),
param('workspaceId').exists().trim(),
body("approvers").isArray(),
validateRequest,
workspaceController.addApproverForWorkspaceAndEnvironment
);
router.delete(
'/:workspaceId/approvers',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN]
}),
param('workspaceId').exists().trim(),
body("approvers").isArray(),
validateRequest,
workspaceController.removeApproverForWorkspaceAndEnvironment
);
router.get(
'/:workspaceId/service-tokens', // deprecate
requireAuth({

@ -22,8 +22,7 @@ import {
router.post(
'/batch',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read', 'write']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -100,8 +99,7 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -117,8 +115,7 @@ router.get(
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -157,8 +154,7 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -186,8 +182,7 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -197,3 +192,5 @@ router.delete(
export default router;

@ -30,22 +30,13 @@ router.post(
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('name').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('encryptedKey').exists().isString().trim(),
body('iv').exists().isString().trim(),
body('tag').exists().isString().trim(),
body('expiresIn').exists().isNumeric(), // measured in ms
body('permissions').isArray({ min: 1 }).custom((value: string[]) => {
const allowedPermissions = ['read', 'write'];
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
if (invalidValues.length > 0) {
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`);
}
return true
}),
body('name').exists().trim(),
body('workspaceId'),
body('environment'),
body('encryptedKey'),
body('iv'),
body('tag'),
body('expiresIn'), // measured in ms
validateRequest,
serviceTokenDataController.createServiceTokenData
);

@ -1,32 +1,16 @@
import mongoose from 'mongoose';
import { getLogger } from '../utils/logger';
import {
initDatabaseHelper,
closeDatabaseHelper
} from '../helpers/database';
import { initDatabaseHelper } from '../helpers/database';
/**
* Class to handle database actions
*/
class DatabaseService {
/**
* Initialize database connection
* @param {Object} obj
* @param {String} obj.mongoURL - mongo connection string
* @returns
*/
static async initDatabase(MONGO_URL: string) {
return await initDatabaseHelper({
mongoURL: MONGO_URL
});
}
/**
* Close database conection
*/
static async closeDatabase() {
return await closeDatabaseHelper();
}
}
export default DatabaseService;

@ -7,6 +7,9 @@ import {
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
/**
* Class to handle integrations
*/

@ -1,44 +1,27 @@
import { PostHog } from 'posthog-node';
import { getLogger } from '../utils/logger';
import {
getNodeEnv,
getTelemetryEnabled,
getPostHogProjectApiKey,
getPostHogHost
NODE_ENV,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
TELEMETRY_ENABLED
} from '../config';
import { getLogger } from '../utils/logger';
/**
* Logs telemetry enable/disable notice.
*/
const logTelemetryMessage = () => {
if(!getTelemetryEnabled()){
getLogger("backend-main").info([
"",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
if(!TELEMETRY_ENABLED){
getLogger("backend-main").info([
"",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
/**
* Return an instance of the PostHog client initialized.
* @returns
*/
const getPostHogClient = () => {
let postHogClient: any;
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(getPostHogProjectApiKey(), {
host: getPostHogHost()
});
}
return postHogClient;
}
export {
logTelemetryMessage,
getPostHogClient
let postHogClient: any;
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, {
host: POSTHOG_HOST
});
}
export default postHogClient;

@ -1,14 +1,13 @@
import DatabaseService from './DatabaseService';
import { logTelemetryMessage, getPostHogClient } from './PostHogClient';
import postHogClient from './PostHogClient';
import BotService from './BotService';
import EventService from './EventService';
import IntegrationService from './IntegrationService';
import TokenService from './TokenService';
export {
logTelemetryMessage,
getPostHogClient,
DatabaseService,
postHogClient,
BotService,
EventService,
IntegrationService,

@ -1,68 +1,55 @@
import nodemailer from 'nodemailer';
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config';
import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL
SMTP_HOST_SOCKETLABS
} from '../variables';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node';
import {
getSmtpHost,
getSmtpUsername,
getSmtpPassword,
getSmtpSecure,
getSmtpPort
} from '../config';
const mailOpts: SMTPConnection.Options = {
host: SMTP_HOST,
port: SMTP_PORT as number
};
if (SMTP_USERNAME && SMTP_PASSWORD) {
mailOpts.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
};
}
if (SMTP_SECURE) {
switch (SMTP_HOST) {
case SMTP_HOST_SENDGRID:
mailOpts.requireTLS = true;
break;
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
if (SMTP_HOST.includes('amazonaws.com')) {
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
} else {
mailOpts.secure = true;
}
break;
}
}
export const initSmtp = () => {
const mailOpts: SMTPConnection.Options = {
host: getSmtpHost(),
port: getSmtpPort()
};
if (getSmtpUsername() && getSmtpPassword()) {
mailOpts.auth = {
user: getSmtpUsername(),
pass: getSmtpPassword()
};
}
if (getSmtpSecure() ? getSmtpSecure() : false) {
switch (getSmtpHost()) {
case SMTP_HOST_SENDGRID:
mailOpts.requireTLS = true;
break;
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
if (getSmtpHost().includes('amazonaws.com')) {
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
} else {
mailOpts.secure = true;
}
break;
}
}
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
@ -73,7 +60,7 @@ export const initSmtp = () => {
.catch((err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${getSmtpHost()}:${getSmtpPort()} \n\t${err}`
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
);
});

@ -4,12 +4,12 @@
*
************************************************************************************************/
import { NODE_ENV } from "../config"
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { Types } from 'mongoose';
import { getNodeEnv } from '../config';
export const createTestUserForDevelopment = async () => {
if (getNodeEnv() === "development") {
if (NODE_ENV === "development") {
const testUserEmail = "test@localhost.local"
const testUserPassword = "testInfisical1"
const testUserId = "63cefa6ec8d3175601cfa980"

@ -19,6 +19,15 @@ export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => n
stack: error?.stack
});
export const UnprocessableEntityError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 422,
type: error?.type ?? 'unprocessable_entity',
message: error?.message ?? 'The server understands the content of the request, but it was unable to process it because it contains invalid data',
context: error?.context,
stack: error?.stack
});
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 401,
@ -27,7 +36,7 @@ export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) =
context: error?.context,
stack: error?.stack
});
export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 403,
@ -46,6 +55,15 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
stack: error?.stack
});
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'resource_not_found',
message: error?.message ?? 'The requested resource was not found',
context: error?.context,
stack: error?.stack
});
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 500,

@ -1,7 +1,7 @@
/* eslint-disable no-console */
import { createLogger, format, transports } from 'winston';
import LokiTransport from 'winston-loki';
import { getLokiHost, getNodeEnv } from '../config';
import { LOKI_HOST, NODE_ENV } from '../config';
const { combine, colorize, label, printf, splat, timestamp } = format;
@ -25,10 +25,10 @@ const createLoggerWithLabel = (level: string, label: string) => {
})
]
//* Add LokiTransport if it's enabled
if(getLokiHost() !== undefined){
if(LOKI_HOST !== undefined){
_transports.push(
new LokiTransport({
host: getLokiHost(),
host: LOKI_HOST,
handleExceptions: true,
handleRejections: true,
batching: true,
@ -37,11 +37,7 @@ const createLoggerWithLabel = (level: string, label: string) => {
format: format.combine(
format.json()
),
labels: {
app: process.env.npm_package_name,
version: process.env.npm_package_version,
environment: getNodeEnv()
},
labels: {app: process.env.npm_package_name, version: process.env.npm_package_version, environment: NODE_ENV},
onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err)
})
)

@ -1,5 +1,5 @@
import { Request } from 'express'
import { getVerboseErrorOutput } from '../config';
import { VERBOSE_ERROR_OUTPUT } from '../config'
export enum LogLevel {
DEBUG = 100,
@ -87,7 +87,7 @@ export default class RequestError extends Error{
}, this.context)
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer
if(!getVerboseErrorOutput()){
if(!VERBOSE_ERROR_OUTPUT){
_context = this._omit(_context, [
'stacktrace',
'exception',

@ -13,7 +13,6 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
@ -25,16 +24,14 @@ import {
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
getIntegrationOptions
INTEGRATION_OPTIONS,
} from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
@ -50,8 +47,7 @@ import {
import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL
SMTP_HOST_SOCKETLABS
} from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
import {
@ -84,7 +80,6 @@ export {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
@ -96,9 +91,7 @@ export {
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
@ -113,11 +106,10 @@ export {
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
getIntegrationOptions,
INTEGRATION_OPTIONS,
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
PLAN_STARTER,
PLAN_PRO,
MFA_METHOD_EMAIL,

@ -1,11 +1,13 @@
import {
getClientIdHeroku,
getClientSlugVercel,
getClientIdNetlify,
getClientIdAzure,
getClientIdGitLab,
getClientIdGitHub
CLIENT_ID_AZURE,
TENANT_ID_AZURE
} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL,
} from "../config";
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
@ -15,7 +17,6 @@ const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = "github";
const INTEGRATION_GITLAB = "gitlab";
const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
@ -26,7 +27,6 @@ const INTEGRATION_SET = new Set([
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
@ -37,18 +37,16 @@ const INTEGRATION_SET = new Set([
const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`;
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
"https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token";
const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
// integration apps endpoints
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
@ -56,160 +54,144 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
const getIntegrationOptions = () => {
const INTEGRATION_OPTIONS = [
{
name: 'Heroku',
slug: 'heroku',
image: 'Heroku.png',
isAvailable: true,
type: 'oauth',
clientId: getClientIdHeroku(),
docsLink: ''
},
{
name: 'Vercel',
slug: 'vercel',
image: 'Vercel.png',
isAvailable: true,
type: 'oauth',
clientId: '',
clientSlug: getClientSlugVercel(),
docsLink: ''
},
{
name: 'Netlify',
slug: 'netlify',
image: 'Netlify.png',
isAvailable: true,
type: 'oauth',
clientId: getClientIdNetlify(),
docsLink: ''
},
{
name: 'GitHub',
slug: 'github',
image: 'GitHub.png',
isAvailable: true,
type: 'oauth',
clientId: getClientIdGitHub(),
docsLink: ''
},
{
name: 'Render',
slug: 'render',
image: 'Render.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Fly.io',
slug: 'flyio',
image: 'Flyio.svg',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'AWS Parameter Store',
slug: 'aws-parameter-store',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'AWS Secret Manager',
slug: 'aws-secret-manager',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
image: 'Microsoft Azure.png',
isAvailable: true,
type: 'oauth',
clientId: getClientIdAzure(),
docsLink: ''
},
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'GitLab',
slug: 'gitlab',
image: 'GitLab.png',
isAvailable: true,
type: 'custom',
clientId: getClientIdGitLab(),
docsLink: ''
},
{
name: 'Travis CI',
slug: 'travisci',
image: 'Travis CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
image: 'Google Cloud Platform.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
}
]
return INTEGRATION_OPTIONS;
}
const INTEGRATION_OPTIONS = [
{
name: 'Heroku',
slug: 'heroku',
image: 'Heroku.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_HEROKU,
docsLink: ''
},
{
name: 'Vercel',
slug: 'vercel',
image: 'Vercel.png',
isAvailable: true,
type: 'oauth',
clientId: '',
clientSlug: CLIENT_SLUG_VERCEL,
docsLink: ''
},
{
name: 'Netlify',
slug: 'netlify',
image: 'Netlify.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_NETLIFY,
docsLink: ''
},
{
name: 'GitHub',
slug: 'github',
image: 'GitHub.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_GITHUB,
docsLink: ''
},
{
name: 'Render',
slug: 'render',
image: 'Render.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Fly.io',
slug: 'flyio',
image: 'Flyio.svg',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'AWS Parameter Store',
slug: 'aws-parameter-store',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'AWS Secret Manager',
slug: 'aws-secret-manager',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Travis CI',
slug: 'travisci',
image: 'Travis CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
image: 'Microsoft Azure.png',
isAvailable: false,
type: 'oauth',
clientId: CLIENT_ID_AZURE,
tenantId: TENANT_ID_AZURE,
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
image: 'Google Cloud Platform.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
}
]
export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
getIntegrationOptions
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_OPTIONS,
};

@ -1,11 +1,9 @@
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
export {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL
SMTP_HOST_SOCKETLABS
}

@ -197,23 +197,6 @@ const generateOpenAPISpec = async () => {
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
},
ServiceTokenData: {
_id: '',
name: '',
workspace: '',
environment: '',
user: {
_id: '',
firstName: '',
lastName: ''
},
expiresAt: '2023-01-13T14:16:12.210Z',
encryptedKey: '',
iv: '',
tag: '',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
}
}
};

@ -1,21 +0,0 @@
import { Server } from 'http';
import main from '../src';
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
import request from 'supertest';
let server: Server;
beforeAll(async () => {
server = await main;
});
afterAll(async () => {
server.close();
});
describe('Healthcheck endpoint', () => {
it('GET /healthcheck should return OK', async () => {
const res = await request(server).get('/healthcheck');
expect(res.status).toEqual(200);
});
});

@ -1,251 +0,0 @@
import { describe, test, expect } from '@jest/globals';
import {
decryptAsymmetric,
decryptSymmetric,
encryptAsymmetric,
encryptSymmetric
} from '../../src/utils/crypto';
describe('Crypto', () => {
describe('encryptAsymmetric', () => {
describe('given all valid publicKey, privateKey and plaintext', () => {
const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw=';
const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0=';
const plaintext = 'secret-message';
test('should encrypt plain text', () => {
const result = encryptAsymmetric({ plaintext, publicKey, privateKey });
expect(result.ciphertext).toBeDefined();
expect(result.nonce).toBeDefined();
});
});
describe('given empty/undefined publicKey', () => {
let publicKey: string;
const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0=';
const plaintext = 'secret-message';
test('should throw error if publicKey is undefined', () => {
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
});
test('should throw error if publicKey is empty string', () => {
publicKey = '';
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
});
});
describe('given empty/undefined privateKey', () => {
const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw=';
let privateKey: string;
const plaintext = 'secret-message';
test('should throw error if privateKey is undefined', () => {
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
});
test('should throw error if privateKey is empty string', () => {
privateKey = '';
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
});
});
describe('given undefined/invalid plaint text', () => {
const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw=';
const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0=';
let plaintext: string;
test('should throw error if plaintext is undefined', () => {
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
});
test('should encrypt plaintext containing special characters', () => {
plaintext = '131@#$%235!@#&*(&123sadfkjadjf';
const result = encryptAsymmetric({
plaintext,
publicKey,
privateKey
});
expect(result.ciphertext).toBeDefined();
expect(result.nonce).toBeDefined();
});
});
});
describe('decryptAsymmetric', () => {
describe('given all valid publicKey, privateKey and plaintext', () => {
const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw=';
const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0=';
const plaintext = 'secret-message';
test('should decrypt the encrypted plaintext', () => {
const encryptedResult = encryptAsymmetric({
plaintext,
publicKey,
privateKey
});
const ciphertext = encryptedResult.ciphertext;
const nonce = encryptedResult.nonce;
const decryptedResult = decryptAsymmetric({
ciphertext,
nonce,
publicKey,
privateKey
});
expect(decryptedResult).toBeDefined();
expect(decryptedResult).toEqual(plaintext);
});
});
describe('given ciphertext or nonce is modified before decrypt', () => {
const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw=';
const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0=';
const plaintext = 'secret-message';
test('should throw error if ciphertext is modified', () => {
const encryptedResult = encryptAsymmetric({
plaintext,
publicKey,
privateKey
});
const ciphertext = '=12adfJ@#52af1231=123'; // modified
const nonce = encryptedResult.nonce;
expect(() => {
decryptAsymmetric({
ciphertext,
nonce,
publicKey,
privateKey
});
}).toThrowError('Failed to perform asymmetric decryption');
});
test('should throw error if nonce is modified', () => {
const encryptedResult = encryptAsymmetric({
plaintext,
publicKey,
privateKey
});
const ciphertext = encryptedResult.ciphertext;
const nonce = '=12adfJ@#52af1231=123'; // modified
expect(() => {
decryptAsymmetric({
ciphertext,
nonce,
publicKey,
privateKey
});
}).toThrowError('Failed to perform asymmetric decryption');
});
});
});
describe('encryptSymmetric', () => {
let plaintext: string;
const key = '7e8ee7e5cc667b9c1829783ad31f36f4';
test('should encrypt plaintext with the given key', () => {
plaintext = 'secret-message';
const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key });
expect(ciphertext).toBeDefined();
expect(iv).toBeDefined();
expect(tag).toBeDefined();
});
test('should throw an error when plaintext is undefined', () => {
const invalidKey = 'invalid-key';
expect(() => {
encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Failed to perform symmetric encryption');
});
test('should throw an error when invalid key is provided', () => {
plaintext = 'secret-message';
const invalidKey = 'invalid-key';
expect(() => {
encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Failed to perform symmetric encryption');
});
});
describe('decryptSymmetric', () => {
const plaintext = 'secret-message';
const key = '7e8ee7e5cc667b9c1829783ad31f36f4';
const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key });
test('should decrypt encrypted plaintext', () => {
const result = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
expect(result).toBeDefined();
expect(result).toEqual(plaintext);
});
test('should fail if ciphertext is modified', () => {
const modifieldCiphertext = 'abcdefghijklmnopqrstuvwxyz';
expect(() => {
decryptSymmetric({
ciphertext: modifieldCiphertext,
iv,
tag,
key
});
}).toThrowError('Failed to perform symmetric decryption');
});
test('should fail if iv is modified', () => {
const modifiedIv = 'abcdefghijklmnopqrstuvwxyz';
expect(() => {
decryptSymmetric({
ciphertext,
iv: modifiedIv,
tag,
key
});
}).toThrowError('Failed to perform symmetric decryption');
});
test('should fail if tag is modified', () => {
const modifiedTag = 'abcdefghijklmnopqrstuvwxyz';
expect(() => {
decryptSymmetric({
ciphertext,
iv,
tag: modifiedTag,
key
});
}).toThrowError('Failed to perform symmetric decryption');
});
test('should throw an error when decryption fails', () => {
const invalidKey = 'invalid-key';
expect(() => {
decryptSymmetric({
ciphertext,
iv,
tag,
key: invalidKey
});
}).toThrowError('Failed to perform symmetric decryption');
});
});
});

@ -1,29 +0,0 @@
import { describe, test, expect } from '@jest/globals';
import { getChannelFromUserAgent } from '../../src/utils/posthog';
describe('posthog getChannelFromUserAgent', () => {
test("should return 'web' when userAgent includes 'mozilla'", () => {
const userAgent =
'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.115 Mobile Safari/537.36';
const channel = getChannelFromUserAgent(userAgent);
expect(channel).toBe('web');
});
test("should return 'cli'", () => {
const userAgent = 'cli';
const channel = getChannelFromUserAgent(userAgent);
expect(channel).toBe('cli');
});
test("should return 'k8-operator'", () => {
const userAgent = 'k8-operator';
const channel = getChannelFromUserAgent(userAgent);
expect(channel).toBe('k8-operator');
});
test('should return undefined if no userAgent', () => {
const userAgent = undefined;
const channel = getChannelFromUserAgent(userAgent);
expect(channel).toBe('other');
});
});

@ -4,13 +4,10 @@ go 1.19
require (
github.com/99designs/keyring v1.2.2
github.com/mattn/go-isatty v0.0.14
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/spf13/cobra v1.6.1
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/crypto v0.6.0
golang.org/x/term v0.5.0
)
@ -25,6 +22,7 @@ require (
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mtibben/percent v0.2.1 // indirect
@ -33,7 +31,7 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/sys v0.5.0 // indirect
)

@ -56,7 +56,6 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
@ -64,16 +63,12 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4=
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
@ -83,7 +78,6 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -109,12 +103,17 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -126,9 +125,13 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

@ -49,11 +49,6 @@ var exportCmd = &cobra.Command{
util.HandleError(err)
}
projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err)
}
format, err := cmd.Flags().GetString("format")
if err != nil {
util.HandleError(err)
@ -74,7 +69,7 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@ -111,7 +106,6 @@ func init() {
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from")
}
// Format according to the format flag

@ -6,6 +6,7 @@ package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models"
@ -95,7 +96,7 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace) error {
return err
}
err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0600)
err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, os.ModePerm)
if err != nil {
return err
}

@ -41,14 +41,13 @@ var loginCmd = &cobra.Command{
PreRun: toggleDebug,
Run: func(cmd *cobra.Command, args []string) {
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring") || strings.Contains(err.Error(), "GetUserCredsFromKeyRing")) {
if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring")) { // if the key can't be found allow them to override
log.Debug(err)
} else if err != nil {
util.HandleError(err)
}
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired { // if you are logged in but not expired
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email)
if err != nil {
util.HandleError(err)
@ -235,16 +234,8 @@ var loginCmd = &cobra.Command{
// clear backed up secrets from prev account
util.DeleteBackupSecrets()
whilte := color.New(color.FgGreen)
boldWhite := whilte.Add(color.Bold)
boldWhite.Printf(">>>> Welcome to Infisical!")
boldWhite.Printf(" You are now logged in as %v <<<< \n", email)
color.Green("Nice! You are logged in as: %v", email)
plainBold := color.New(color.Bold)
plainBold.Println("\nQuick links")
fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
},
}

@ -36,9 +36,6 @@ var resetCmd = &cobra.Command{
keyringInstance.Remove(util.KEYRING_SERVICE_NAME)
// delete secrets backup
util.DeleteBackupSecrets()
util.PrintSuccessMessage("Reset successful")
},
}

@ -186,11 +186,14 @@ func init() {
// Will execute a single command and pass in the given secrets into the process
func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error {
command := args[0]
argsForCommand := args[1:]
shell := subShellCmd()
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
cmd := exec.Command(command, argsForCommand...)
args = append(args[:1], args[0:]...) // shift args to the right
args[0] = shell[1]
cmd := exec.Command(shell[0], args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -200,15 +203,7 @@ func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string)
}
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
shell := [2]string{"sh", "-c"}
if runtime.GOOS == "windows" {
shell = [2]string{"cmd", "/C"}
} else {
currentShell := os.Getenv("SHELL")
if currentShell != "" {
shell[0] = currentShell
}
}
shell := subShellCmd()
cmd := exec.Command(shell[0], shell[1], fullCommand)
cmd.Stdin = os.Stdin
@ -222,6 +217,23 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
return execCmd(cmd)
}
func subShellCmd() [2]string {
// default to sh -c
shell := [...]string{"sh", "-c"}
currentShell := os.Getenv("SHELL")
if currentShell != "" {
shell[0] = currentShell
} else if runtime.GOOS == "windows" {
// if the SHELL env var is not set and we're on Windows, use cmd.exe
// The SHELL var should always be checked first, in case the user executes
// infisical from something like Git Bash.
return [...]string{"cmd", "/C"}
}
return shell
}
// Credit: inspired by AWS Valut
func execCmd(cmd *exec.Cmd) error {
sigChannel := make(chan os.Signal, 1)

@ -19,7 +19,6 @@ import (
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -133,11 +132,6 @@ var secretsSetCmd = &cobra.Command{
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
log.Debugf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
util.PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
}
// decrypt workspace key
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
@ -263,10 +257,10 @@ var secretsSetCmd = &cobra.Command{
}
// Print secret operations
headers := [...]string{"SECRET NAME", "SECRET VALUE", "STATUS"}
rows := [][3]string{}
headers := []string{"SECRET NAME", "SECRET VALUE", "STATUS"}
rows := [][]string{}
for _, secretOperation := range secretOperations {
rows = append(rows, [...]string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation})
rows = append(rows, []string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation})
}
visualize.Table(headers, rows)

@ -55,5 +55,4 @@ type GetAllSecretsParameters struct {
EnvironmentPassedViaFlag bool
InfisicalToken string
TagSlugs string
WorkspaceId string
}

@ -4,47 +4,18 @@ import (
"encoding/json"
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"runtime"
"github.com/fatih/color"
)
func CheckForUpdate() {
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
return
}
latestVersion, err := getLatestTag("Infisical", "infisical")
if err != nil {
log.Debug(err)
// do nothing and continue
return
}
if latestVersion != CLI_VERSION {
yellow := color.New(color.FgYellow).SprintFunc()
blue := color.New(color.FgCyan).SprintFunc()
black := color.New(color.FgBlack).SprintFunc()
msg := fmt.Sprintf("%s %s %s %s",
yellow("A new release of infisical is available:"),
blue(CLI_VERSION),
black("->"),
blue(latestVersion),
)
fmt.Fprintln(os.Stderr, msg)
updateInstructions := GetUpdateInstructions()
if updateInstructions != "" {
msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions())
fmt.Fprintln(os.Stderr, msg)
}
PrintWarning(fmt.Sprintf("Please update your CLI. You are running version %s but the latest version is %s", CLI_VERSION, latestVersion))
}
}
@ -55,12 +26,12 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
return "", err
}
if resp.StatusCode != 200 {
return "", errors.New(fmt.Sprintf("gitHub API returned status code %d", resp.StatusCode))
return "", errors.New(fmt.Sprintf("GitHub API returned status code %d", resp.StatusCode))
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
@ -69,59 +40,7 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
Name string `json:"name"`
}
if err := json.Unmarshal(body, &tags); err != nil {
return "", fmt.Errorf("failed to unmarshal github response: %w", err)
}
json.Unmarshal(body, &tags)
return tags[0].Name[1:], nil
}
func GetUpdateInstructions() string {
os := runtime.GOOS
switch os {
case "darwin":
return "To update, run: brew update && brew upgrade infisical"
case "windows":
return "To update, run: scoop update infisical"
case "linux":
pkgManager := getLinuxPackageManager()
switch pkgManager {
case "apt-get":
return "To update, run: sudo apt-get update && sudo apt-get install infisical"
case "yum":
return "To update, run: sudo yum update infisical"
case "apk":
return "To update, run: sudo apk update && sudo apk upgrade infisical"
case "yay":
return "To update, run: yay -Syu infisical"
default:
return ""
}
default:
return ""
}
}
func getLinuxPackageManager() string {
cmd := exec.Command("apt-get", "--version")
if err := cmd.Run(); err == nil {
return "apt-get"
}
cmd = exec.Command("yum", "--version")
if err := cmd.Run(); err == nil {
return "yum"
}
cmd = exec.Command("yay", "--version")
if err := cmd.Run(); err == nil {
return "yay"
}
cmd = exec.Command("apk", "--version")
if err := cmd.Run(); err == nil {
return "apk"
}
return ""
}

@ -42,7 +42,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
}
// Create file in directory
err = WriteToFile(fullConfigFilePath, configFileMarshalled, 0600)
err = WriteToFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
if err != nil {
return err
}
@ -151,6 +151,52 @@ func GetWorkspaceConfigByPath(path string) (workspaceConfig models.WorkspaceConf
return workspaceConfigFile, nil
}
// Will get the list of .infisical.json files that are located
// within the root of each sub folder from where the CLI is ran from
func GetAllWorkSpaceConfigsStartingFromCurrentPath() (workspaces []models.WorkspaceConfigFile, err error) {
currentDir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("GetAllProjectConfigs: unable to get the current directory because [%s]", err)
}
files, err := os.ReadDir(currentDir)
if err != nil {
return nil, fmt.Errorf("GetAllProjectConfigs: unable to read the contents of the current directory because [%s]", err)
}
listOfWorkSpaceConfigs := []models.WorkspaceConfigFile{}
for _, file := range files {
if !file.IsDir() && file.Name() == INFISICAL_WORKSPACE_CONFIG_FILE_NAME {
pathToWorkspaceConfigFile := currentDir + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME
workspaceConfig, err := GetWorkspaceConfigByPath(pathToWorkspaceConfigFile)
if err != nil {
return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err)
}
listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig)
} else if file.IsDir() {
pathToSubFolder := currentDir + "/" + file.Name()
pathToMaybeWorkspaceConfigFile := pathToSubFolder + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME
_, err := os.Stat(pathToMaybeWorkspaceConfigFile)
if err != nil {
continue // workspace config file doesn't exist
}
workspaceConfig, err := GetWorkspaceConfigByPath(pathToMaybeWorkspaceConfigFile)
if err != nil {
return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err)
}
listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig)
}
}
return listOfWorkSpaceConfigs, nil
}
// Get the infisical config file and if it doesn't exist, return empty config model, otherwise raise error
func GetConfigFile() (models.ConfigFile, error) {
fullConfigFilePath, _, err := GetFullConfigFilePath()
@ -197,7 +243,7 @@ func WriteConfigFile(configFile *models.ConfigFile) error {
}
// Create file in directory
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, 0600)
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
if err != nil {
return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err)
}

@ -20,9 +20,6 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
}
}
supportMsg := fmt.Sprintf("\n\nIf this issue continues, get support at https://infisical.com/slack")
fmt.Fprintln(os.Stderr, supportMsg)
os.Exit(exitCode)
}

Some files were not shown because too many files have changed in this diff Show More