mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Compare commits
154 Commits
approvals-
...
update-rea
Author | SHA1 | Date | |
---|---|---|---|
d83220cdc3 | |||
8aa44ba103 | |||
119600b64f | |||
18bbe09af4 | |||
b944e8bb84 | |||
0b2e6a0d77 | |||
51f4ab473b | |||
9e8e538647 | |||
809a551073 | |||
83e1900d89 | |||
9b2a31761a | |||
87c99df13d | |||
8adc53a8bc | |||
1487afb36b | |||
2bbcd3d9e6 | |||
fb1f93a3c0 | |||
1489604f82 | |||
fdae5105f8 | |||
8e55d17a55 | |||
ded5c50157 | |||
46f2b7a3f8 | |||
3d818f953d | |||
3ac98ba326 | |||
bb2bcb8bd1 | |||
d103c81f67 | |||
b591d638d0 | |||
778631f396 | |||
2ec3143d27 | |||
d705440400 | |||
e6e3d82fa6 | |||
db48ab8f6c | |||
b868b6a5f3 | |||
dabc7e3eb1 | |||
38efb6a1e2 | |||
a6c8638345 | |||
31111fc63b | |||
7fd06e36bc | |||
5c55e6e508 | |||
71fe15d56e | |||
0a71c993ed | |||
63adc181c8 | |||
76fc82811a | |||
1e859c19f4 | |||
175b4a3fb6 | |||
d89976802d | |||
dce5c8f621 | |||
d8ff36f59f | |||
1090a61162 | |||
0e11ff198c | |||
cdbc6f5619 | |||
78cb18ad0e | |||
42374a775d | |||
0269b58a3c | |||
ef4a316558 | |||
a676ce7c21 | |||
f475daf7a6 | |||
c8110c31ef | |||
a5c8c9c279 | |||
5860136494 | |||
3f3516b7ba | |||
06e26da684 | |||
bb70ff96d2 | |||
c019d57fb6 | |||
7854a5eea2 | |||
29636173ef | |||
4edfc1e0be | |||
61d4da49aa | |||
56187ec43e | |||
971ac26033 | |||
1f316a0b65 | |||
23d09c37b5 | |||
fc7c3022be | |||
5b65adedbb | |||
6faf9bf4bf | |||
b5998d7f22 | |||
6abbc1c54d | |||
85e5319981 | |||
50da0a753a | |||
6a5f2d0566 | |||
d93277155f | |||
cb905e5ee6 | |||
71261e7594 | |||
27e4f490d3 | |||
298c8705d7 | |||
edc4382a48 | |||
5baab76f2e | |||
f9f30efe03 | |||
12701bdf98 | |||
70967ac7b0 | |||
98b443da82 | |||
10f75c8e55 | |||
b226642853 | |||
933f837f64 | |||
7327698305 | |||
1dc59d0d41 | |||
13e067dc4f | |||
885d348f96 | |||
c71ee77503 | |||
14206de926 | |||
c73d64d784 | |||
913067f014 | |||
ff2ee989d6 | |||
200cefc1b2 | |||
721af0f26d | |||
aca6269920 | |||
a4074c9687 | |||
205ec61549 | |||
0d16f707c2 | |||
d3d5ead6ed | |||
1f05d6ea4d | |||
ff82af8358 | |||
a7da858694 | |||
b5c2f6e551 | |||
77226e0924 | |||
0cc4286f5f | |||
99144143ff | |||
efff841121 | |||
2f8d914ecb | |||
7dd28a5941 | |||
a89fccdc1f | |||
40ddd3b2a5 | |||
74d17a20a4 | |||
d537bd2f58 | |||
2f045be8a4 | |||
c5ee4810ad | |||
1dbda5876f | |||
d948923d95 | |||
fb1085744a | |||
ec22291aca | |||
00a07fd27c | |||
ec0e77cc5a | |||
16c49a9626 | |||
06ea809d60 | |||
12364005c1 | |||
98573e9e05 | |||
c1a4ca6203 | |||
21c2fd8542 | |||
b27bc8fc1b | |||
091115e6ba | |||
d9c055872d | |||
f73d18ddc7 | |||
eb47126f68 | |||
4750767268 | |||
b0ed772885 | |||
7fdab81b5f | |||
c17bf13f8c | |||
515e010065 | |||
2c46e8a2dc | |||
eebe3c164a | |||
d7acd7aef6 | |||
860b8efd7d | |||
6ca3fc5ad2 | |||
189af07ff5 | |||
caf7426f86 |
.env.exampleREADME.md
backend
__tests__
environment.d.tsjest.config.tspackage-lock.jsonpackage.jsonspec.jsonsrc
app.ts
config
controllers
v1
authController.tsindex.tsintegrationAuthController.tsintegrationController.tsmembershipController.tsmembershipOrgController.tsorganizationController.tspasswordController.tssecretApprovalController.tssecretController.tsserviceTokenController.tssignupController.tsstripeController.tsworkspaceController.ts
v2
ee
helpers
index.tsintegrations
middleware
requestErrorHandler.tsrequireAuth.tsrequireMfaAuth.tsrequireSecretsAuth.tsrequireServiceTokenAuth.tsrequireSignupAuth.tsrequireWorkspaceAuth.ts
models
integration.tsintegrationAuth.tssecret.tssecretApprovalRequest.tsserviceTokenData.tstoken.tsworkspace.ts
routes
status
v1
v2
services
utils
variables
swagger
tests
cli
cloudformation/ec2-deployment
docker-compose.dev.ymldocs
api-reference
endpoints/service-tokens
overview
cli
getting-started
images
deploy-aws-button.pngintegrations-azure-key-vault-create.pngintegrations-azure-key-vault-vault-uri.pngintegrations-azure-key-vault.pngintegrations-gitlab-auth.pngintegrations-gitlab-create.pngintegrations-gitlab.pngintegrations.png
integrations
mint.jsonsdks
self-hosting
spec.yamlfrontend
next-i18next.config.jspackage-lock.jsonpackage.jsontsconfig.json
public
data
images/integrations
json
locales
en
es
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-api-key.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.json
pt-BR
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonmfa.jsonsection-incident.jsonsection-members.jsonsettings-org.jsonsettings-project.jsonsignup.json
pt-PT
src
components
helpers
hooks/api
integrationAuth
serverDetails
serviceTokens
users
layouts/AppLayout
pages
activity
dashboard
integrations
login.tsxsettings
signup.tsxverify-email.tsxviews/Settings
helm-charts/secrets-operator
i18n
k8-operator
nginx
@ -16,9 +16,6 @@ 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
|
||||
@ -48,10 +45,12 @@ 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
|
||||
|
99
README.md
99
README.md
@ -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-36.7k-orange" alt="Cloudsmith downloads" />
|
||||
<img src="https://img.shields.io/badge/Downloads-45.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,37 +44,100 @@
|
||||
<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.
|
||||
|
||||
- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's secrets and configs within projects
|
||||
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects esecrets and configs into your local workflow
|
||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
||||
- **Personal overrides** for secrets and configs
|
||||
- **[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
|
||||
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
|
||||
- **[Audit 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)
|
||||
- 🔜 **1-Click Deploy** to AWS
|
||||
- 🔜 **Dynamic Secrets**
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
- 🔜 **Smart Security Alerts**
|
||||
- 🔜 **Secrets Rotation**
|
||||
- 🔜 **Slack & MS Teams** integrations
|
||||
|
||||
And more.
|
||||
## 🚀 Quick start: Inject secrets into your application
|
||||
There are several methods for integrating Infisical secrets into your application. You can sync secrets to third-party services using integrations like AWS Secrets Manager, GCP, Render, and others. Additionally, you can use Infisical SDKs to retrieve secrets directly via code. For Kubernetes users, utilize the secrets operator for seamless integration. Lastly, our CLI allows you to inject secrets directly into your application as environment variables.
|
||||
|
||||
## 🚀 Get started
|
||||
## Install Infisical CLI
|
||||
|
||||
To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction).
|
||||
#### MacOs
|
||||
Use [brew](https://brew.sh/) package manager
|
||||
|
||||
<p>
|
||||
```bash
|
||||
brew install infisical/get-cli/infisical
|
||||
```
|
||||
|
||||
#### Windows
|
||||
Use [Scoop](https://scoop.sh/) package manager
|
||||
|
||||
```bash
|
||||
scoop bucket add org https://github.com/Infisical/scoop-infisical.git
|
||||
```
|
||||
|
||||
```bash
|
||||
scoop install infisical
|
||||
```
|
||||
|
||||
#### Debian/Ubuntu
|
||||
```bash
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y infisical
|
||||
```
|
||||
|
||||
Install CLI on [other platforms](https://infisical.com/docs/cli/overview)
|
||||
|
||||
### Authenticate
|
||||
After installing the CLI on your system, you'll need to authenticate with your Infisical credentials. If you're developing locally, you can log in directly using infisical login.
|
||||
However, if you're in a non-development environment, you can authenticate using a [service token](https://infisical.com/docs/getting-started/dashboard/token).
|
||||
|
||||
|
||||
### Inject secrets into your application process
|
||||
To feed secrets from Infisical into your application process, you can start your appliciaon with Infisical CLI. By starting your application with the Infisical CLI, the secrets are injected into your application process as environment variables, enabling your application to access them from the environment.
|
||||
|
||||
```bash
|
||||
## Inject secrets into a single command ##
|
||||
infisical run -- [your application start command]
|
||||
|
||||
# Example
|
||||
infisical run -- npm run start
|
||||
|
||||
## Inject secrets into multiple commands ##
|
||||
infisical run [options] --command [string command]
|
||||
|
||||
# Example
|
||||
infisical run [options] --command "npm run bootstrap && npm run dev start; other-bash-command"
|
||||
```
|
||||
|
||||
View all available commands for Infisical CLI [here](https://infisical.com/docs/cli/commands/run)
|
||||
|
||||
## Self host Infisical
|
||||
To meet various compliance requirements, you may want to self-host Infisical instead of using Infisical Cloud.
|
||||
Self-hosted Infisical allows you to maintain your sensitive information within your own infrastructure and network, ensuring complete control over your data.
|
||||
|
||||
### One click EC2 deployment
|
||||
|
||||
<img src="https://mintlify.s3-us-west-1.amazonaws.com/infisical/images/deploy-aws-button.png" height=80 />
|
||||
|
||||
### Kubernetes Helm deployment
|
||||
|
||||
|
||||
<!-- To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction). -->
|
||||
|
||||
<!-- <p>
|
||||
<a href="https://infisical.com/docs/self-hosting/overview" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/218910609-18a75846-51a9-420a-a9a9-5958ac9c5505.png" height=150 /> </a>
|
||||
<a href="https://app.infisical.com/signup" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/218910520-b36a607f-af66-4a06-af10-6a2191ab02de.png" height=150></a>
|
||||
</p>
|
||||
</p> -->
|
||||
|
||||
## 🔥 What's cool about this?
|
||||
|
||||
@ -157,7 +220,9 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
|
||||
<a href="https://infisical.com/docs/integrations/cicd/circleci?ref=github.com">
|
||||
✔️ CircleCI
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -168,7 +233,9 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
🔜 Digital Ocean
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Azure
|
||||
<a href="https://infisical.com/docs/integrations/cloud/azure-key-vault?ref=github.com">
|
||||
✔️ Azure Key Vault
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -1,19 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
3
backend/environment.d.ts
vendored
3
backend/environment.d.ts
vendored
@ -4,7 +4,6 @@ declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PORT: string;
|
||||
EMAIL_TOKEN_LIFETIME: string;
|
||||
ENCRYPTION_KEY: string;
|
||||
SALT_ROUNDS: string;
|
||||
JWT_AUTH_LIFETIME: string;
|
||||
@ -22,10 +21,12 @@ 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;
|
||||
|
9
backend/jest.config.ts
Normal file
9
backend/jest.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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']
|
||||
};
|
9591
backend/package-lock.json
generated
9591
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@
|
||||
"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",
|
||||
@ -87,7 +88,7 @@
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
@ -100,17 +101,6 @@
|
||||
"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
@ -1,149 +0,0 @@
|
||||
// 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,103 +1,51 @@
|
||||
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
|
||||
};
|
||||
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
|
@ -1,8 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import * as Sentry from '@sentry/node';
|
||||
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: NODE_ENV === 'production' ? true : false
|
||||
secure: getNodeEnv() === '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: NODE_ENV === 'production' ? true : false
|
||||
secure: getNodeEnv() === '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, JWT_REFRESH_SECRET)
|
||||
jwt.verify(refreshToken, getJwtRefreshSecret())
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: decodedToken.userId
|
||||
},
|
||||
expiresIn: JWT_AUTH_LIFETIME,
|
||||
secret: JWT_AUTH_SECRET
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
|
@ -14,7 +14,6 @@ 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,
|
||||
@ -32,6 +31,5 @@ export {
|
||||
stripeController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretApprovalController
|
||||
workspaceController
|
||||
};
|
||||
|
@ -2,13 +2,16 @@ import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Bot
|
||||
} from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
import {
|
||||
getApps,
|
||||
getTeams,
|
||||
revokeAccess
|
||||
} from '../../integrations';
|
||||
|
||||
/***
|
||||
* Return integration authorization with id [integrationAuthId]
|
||||
@ -36,9 +39,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
integrationOptions: INTEGRATION_OPTIONS,
|
||||
});
|
||||
const INTEGRATION_OPTIONS = getIntegrationOptionsFunc();
|
||||
|
||||
return res.status(200).send({
|
||||
integrationOptions: INTEGRATION_OPTIONS,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -154,25 +159,54 @@ export const saveIntegrationAccessToken = async (
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
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",
|
||||
});
|
||||
}
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
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,10 +2,7 @@ import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration,
|
||||
Workspace,
|
||||
Bot,
|
||||
BotKey
|
||||
Integration
|
||||
} from '../../models';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
@ -18,6 +15,7 @@ import { eventPushSecrets } from '../../events';
|
||||
*/
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
|
||||
try {
|
||||
const {
|
||||
integrationAuthId,
|
||||
@ -34,19 +32,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 { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models';
|
||||
import { Request, Response } from 'express';
|
||||
import { Membership, MembershipOrg, User, Key } 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: SITE_URL + '/login'
|
||||
callback_url: getSiteURL() + '/login'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
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';
|
||||
@ -8,6 +7,7 @@ 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,9 +99,11 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
let invitee, inviteeMembershipOrg;
|
||||
let invitee, inviteeMembershipOrg, completeInviteLink;
|
||||
try {
|
||||
const { organizationId, inviteeEmail } = req.body;
|
||||
const host = req.headers.host;
|
||||
const siteUrl = `${req.protocol}://${host}`;
|
||||
|
||||
// validate membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
@ -178,9 +180,13 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
organizationName: organization.name,
|
||||
email: inviteeEmail,
|
||||
token,
|
||||
callback_url: SITE_URL + '/signupinvite'
|
||||
callback_url: getSiteURL() + '/signupinvite'
|
||||
}
|
||||
});
|
||||
|
||||
if (!getSmtpConfigured()) {
|
||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
|
||||
}
|
||||
}
|
||||
|
||||
await updateSubscriptionOrgQuantity({ organizationId });
|
||||
@ -193,7 +199,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an invite link to ${req.body.inviteeEmail}`
|
||||
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
||||
completeInviteLink
|
||||
});
|
||||
};
|
||||
|
||||
@ -218,7 +225,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,
|
||||
@ -250,8 +257,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||
secret: JWT_SIGNUP_SECRET
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -1,26 +1,18 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
SITE_URL,
|
||||
STRIPE_SECRET_KEY
|
||||
} from '../../config';
|
||||
import { Request, Response } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
import {
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Organization,
|
||||
Workspace,
|
||||
IncidentContactOrg,
|
||||
IMembershipOrg
|
||||
IncidentContactOrg
|
||||
} 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;
|
||||
@ -325,6 +317,10 @@ 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,
|
||||
@ -337,13 +333,13 @@ export const createOrganizationPortalSession = async (
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
success_url: SITE_URL + '/dashboard',
|
||||
cancel_url: SITE_URL + '/dashboard'
|
||||
success_url: getSiteURL() + '/dashboard',
|
||||
cancel_url: getSiteURL() + '/dashboard'
|
||||
});
|
||||
} else {
|
||||
session = await stripe.billingPortal.sessions.create({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
return_url: SITE_URL + '/dashboard'
|
||||
return_url: getSiteURL() + '/dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
@ -369,6 +365,10 @@ 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: SITE_URL + '/password-reset'
|
||||
callback_url: getSiteURL() + '/password-reset'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||
secret: JWT_SIGNUP_SECRET
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -1,320 +0,0 @@
|
||||
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 { postHogClient } from '../../services';
|
||||
import { getPostHogClient } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@ -38,6 +38,7 @@ 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;
|
||||
@ -111,6 +112,7 @@ 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;
|
||||
@ -179,6 +181,7 @@ 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 { JWT_SERVICE_SECRET } from '../../config';
|
||||
import { getJwtServiceSecret } from '../../config';
|
||||
|
||||
/**
|
||||
* Return service token on request
|
||||
@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
||||
workspaceId
|
||||
},
|
||||
expiresIn: expiresIn,
|
||||
secret: JWT_SERVICE_SECRET
|
||||
secret: getJwtServiceSecret()
|
||||
});
|
||||
} 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 (INVITE_ONLY_SIGNUP) {
|
||||
if (getInviteOnlySignup()) {
|
||||
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
||||
const userCount = await User.countDocuments({})
|
||||
if (userCount != 0) {
|
||||
@ -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 });
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
@ -75,10 +75,12 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
// verify email
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
if (getSmtpConfigured()) {
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
@ -91,8 +93,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||
secret: JWT_SIGNUP_SECRET
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../config';
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
||||
|
||||
/**
|
||||
* Handle service provisioning/un-provisioning via Stripe
|
||||
@ -16,11 +13,15 @@ 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,
|
||||
STRIPE_WEBHOOK_SECRET // ?
|
||||
getStripeWebhookSecret()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
|
@ -16,8 +16,7 @@ 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
|
||||
@ -304,112 +303,6 @@ 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,13 +1,11 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
APIKeyData
|
||||
} from '../../models';
|
||||
import {
|
||||
SALT_ROUNDS
|
||||
} from '../../config';
|
||||
import { getSaltRounds } from '../../config';
|
||||
|
||||
/**
|
||||
* Return API key data for user with id [req.user_id]
|
||||
@ -45,7 +43,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, SALT_ROUNDS);
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
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,8 +28,6 @@ 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
|
||||
@ -89,7 +87,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;
|
||||
@ -126,15 +124,15 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: JWT_MFA_LIFETIME,
|
||||
secret: JWT_MFA_SECRET
|
||||
expiresIn: getJwtMfaLifetime(),
|
||||
secret: getJwtMfaSecret()
|
||||
});
|
||||
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
@ -144,13 +142,13 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.ip,
|
||||
@ -165,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
});
|
||||
|
||||
// case: user does not have MFA enablgged
|
||||
@ -183,7 +181,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
|
||||
const response: ResponseData = {
|
||||
mfaEnabled: false,
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
@ -193,7 +191,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
user?.protectedKey &&
|
||||
user?.protectedKeyIV &&
|
||||
@ -208,14 +206,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);
|
||||
}
|
||||
|
||||
@ -246,7 +244,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',
|
||||
@ -261,9 +259,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'
|
||||
});
|
||||
@ -276,76 +274,87 @@ 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: 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;
|
||||
}
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
interface VerifyMfaTokenRes {
|
||||
encryptionVersion: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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 { postHogClient } from '../../services';
|
||||
import { getPostHogClient } from '../../services';
|
||||
|
||||
/**
|
||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||
@ -15,6 +15,7 @@ import { postHogClient } 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 = {
|
||||
@ -67,6 +68,7 @@ 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[] = []
|
||||
@ -128,6 +130,7 @@ 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
|
||||
|
||||
@ -181,6 +184,7 @@ 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) {
|
||||
@ -209,6 +213,7 @@ 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())
|
||||
@ -276,6 +281,7 @@ 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;
|
||||
|
||||
@ -329,6 +335,7 @@ 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 { postHogClient } from '../../services';
|
||||
import { getPostHogClient } 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,6 +33,8 @@ import {
|
||||
*/
|
||||
export const batchSecrets = async (req: Request, res: Response) => {
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent']);
|
||||
const postHogClient = getPostHogClient();
|
||||
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
@ -41,13 +43,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':
|
||||
@ -70,7 +72,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// handle create secrets
|
||||
let createdSecrets: ISecret[] = [];
|
||||
if (createSecrets.length > 0) {
|
||||
@ -109,18 +111,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
|
||||
@ -140,7 +142,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,
|
||||
@ -227,7 +229,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (actions.length > 0) {
|
||||
// (EE) create (audit) log
|
||||
await EELogService.createLog({
|
||||
@ -250,7 +252,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
|
||||
const resObj: { [key: string]: ISecret[] | string[] } = {}
|
||||
|
||||
if (createSecrets.length > 0) {
|
||||
@ -260,11 +262,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);
|
||||
}
|
||||
|
||||
@ -326,6 +328,7 @@ 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;
|
||||
@ -358,9 +361,25 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const newlyCreatedSecrets = await Secret.insertMany(
|
||||
listOfSecretsToCreate.map(({
|
||||
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),
|
||||
type,
|
||||
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
@ -371,26 +390,10 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
}: 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
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
|
||||
|
||||
setTimeout(async () => {
|
||||
// trigger event - push secrets
|
||||
@ -530,6 +533,7 @@ 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(',') : [];
|
||||
@ -732,6 +736,7 @@ 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
|
||||
@ -953,6 +958,7 @@ 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,15 +1,13 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
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
|
||||
@ -17,7 +15,35 @@ import { ABILITY_READ } from '../../variables/organization';
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
@ -28,6 +54,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceToken, serviceTokenData;
|
||||
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
@ -36,7 +63,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
permissions
|
||||
} = req.body;
|
||||
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
|
||||
@ -45,7 +73,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
@ -59,7 +87,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
@ -111,4 +140,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 (process.env.LOOPS_API_KEY) {
|
||||
if (getLoopsApiKey()) {
|
||||
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 " + process.env.LOOPS_API_KEY
|
||||
"Authorization": "Bearer " + getLoopsApiKey()
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
secure: getNodeEnv() === '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: NODE_ENV === 'production' ? true : false
|
||||
secure: getNodeEnv() === '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('+publicKey +encryptedPrivateKey +iv +tag');
|
||||
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
||||
} 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 { postHogClient, EventService } from '../../services';
|
||||
import { getPostHogClient, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
|
||||
interface V2PushSecret {
|
||||
@ -48,6 +48,7 @@ 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;
|
||||
@ -121,6 +122,7 @@ 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,10 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
||||
|
||||
/**
|
||||
* Handle service provisioning/un-provisioning via Stripe
|
||||
@ -15,12 +12,16 @@ const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
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,
|
||||
STRIPE_WEBHOOK_SECRET // ?
|
||||
getStripeWebhookSecret()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { LICENSE_KEY } from '../../config';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition license actions
|
||||
*/
|
||||
@ -16,4 +14,4 @@ class EELicenseService {
|
||||
}
|
||||
}
|
||||
|
||||
export default new EELicenseService(LICENSE_KEY);
|
||||
export default new EELicenseService('N/A');
|
@ -1,17 +1,12 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import jwt from 'jsonwebtoken';
|
||||
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,
|
||||
@ -19,6 +14,12 @@ import {
|
||||
UnauthorizedRequestError,
|
||||
BadRequestError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
getJwtAuthLifetime,
|
||||
getJwtAuthSecret,
|
||||
getJwtRefreshLifetime,
|
||||
getJwtRefreshSecret
|
||||
} from '../config';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -92,7 +93,7 @@ const getAuthUserPayload = async ({
|
||||
let user;
|
||||
try {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
|
||||
jwt.verify(authTokenValue, getJwtAuthSecret())
|
||||
);
|
||||
|
||||
user = await User.findOne({
|
||||
@ -148,7 +149,10 @@ const getAuthSTDPayload = async ({
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER)
|
||||
.select('+encryptedKey +iv +tag').populate('user');
|
||||
.select('+encryptedKey +iv +tag')
|
||||
.populate<{user: IUser}>('user');
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
|
||||
} catch (err) {
|
||||
throw UnauthorizedRequestError({
|
||||
@ -220,16 +224,16 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: JWT_AUTH_LIFETIME,
|
||||
secret: JWT_AUTH_SECRET
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
});
|
||||
|
||||
refreshToken = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: JWT_REFRESH_LIFETIME,
|
||||
secret: JWT_REFRESH_SECRET
|
||||
expiresIn: getJwtRefreshLifetime(),
|
||||
secret: getJwtRefreshSecret()
|
||||
});
|
||||
} 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: ENCRYPTION_KEY
|
||||
key: getEncryptionKey()
|
||||
});
|
||||
|
||||
bot = await new Bot({
|
||||
@ -130,7 +130,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
key: ENCRYPTION_KEY
|
||||
key: getEncryptionKey()
|
||||
});
|
||||
|
||||
key = decryptAsymmetric({
|
||||
|
@ -29,6 +29,23 @@ 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
|
||||
initDatabaseHelper,
|
||||
closeDatabaseHelper
|
||||
}
|
@ -229,7 +229,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||
accessToken = await exchangeRefresh({
|
||||
integration: integrationAuth.integration,
|
||||
integrationAuth,
|
||||
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 { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { getSmtpFromName, getSmtpFromAddress, getSmtpConfigured } from '../config';
|
||||
|
||||
let smtpTransporter: nodemailer.Transporter;
|
||||
|
||||
@ -25,23 +25,25 @@ const sendMail = async ({
|
||||
recipients: string[];
|
||||
substitutions: any;
|
||||
}) => {
|
||||
try {
|
||||
const html = fs.readFileSync(
|
||||
path.resolve(__dirname, '../templates/' + template),
|
||||
'utf8'
|
||||
);
|
||||
const temp = handlebars.compile(html);
|
||||
const htmlToSend = temp(substitutions);
|
||||
if (getSmtpConfigured()) {
|
||||
try {
|
||||
const html = fs.readFileSync(
|
||||
path.resolve(__dirname, '../templates/' + template),
|
||||
'utf8'
|
||||
);
|
||||
const temp = handlebars.compile(html);
|
||||
const htmlToSend = temp(substitutions);
|
||||
|
||||
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);
|
||||
await smtpTransporter.sendMail({
|
||||
from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`,
|
||||
to: recipients.join(', '),
|
||||
subject: subjectLine,
|
||||
html: htmlToSend
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,23 +1,14 @@
|
||||
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';
|
||||
|
||||
const productToPriceMap = {
|
||||
starter: STRIPE_PRODUCT_STARTER,
|
||||
team: STRIPE_PRODUCT_TEAM,
|
||||
pro: STRIPE_PRODUCT_PRO
|
||||
};
|
||||
import {
|
||||
getStripeSecretKey,
|
||||
getStripeProductPro,
|
||||
getStripeProductTeam,
|
||||
getStripeProductStarter
|
||||
} from '../config';
|
||||
|
||||
/**
|
||||
* Create an organization with name [name]
|
||||
@ -36,8 +27,11 @@ const createOrganization = async ({
|
||||
let organization;
|
||||
try {
|
||||
// register stripe account
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
if (STRIPE_SECRET_KEY) {
|
||||
if (getStripeSecretKey()) {
|
||||
const customer = await stripe.customers.create({
|
||||
email,
|
||||
description: name
|
||||
@ -87,6 +81,16 @@ 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: [
|
||||
@ -139,6 +143,10 @@ const updateSubscriptionOrgQuantity = async ({
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
const subscription = (
|
||||
await stripe.subscriptions.list({
|
||||
customer: organization.customerId
|
||||
@ -167,4 +175,4 @@ export {
|
||||
createOrganization,
|
||||
initSubscriptionOrg,
|
||||
updateSubscriptionOrgQuantity
|
||||
};
|
||||
};
|
@ -267,7 +267,7 @@ const v1PushSecrets = async ({
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
const newSecrets = await Secret.insertMany(
|
||||
const newSecrets: ISecret[] = (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,27 +713,10 @@ 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,
|
||||
secretObjectHasRequiredFields
|
||||
reformatPullSecrets
|
||||
};
|
||||
|
@ -9,10 +9,8 @@ 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]
|
||||
@ -86,7 +84,7 @@ const createTokenHelper = async ({
|
||||
const query: TokenDataQuery = { type };
|
||||
const update: TokenDataUpdate = {
|
||||
type,
|
||||
tokenHash: await bcrypt.hash(token, SALT_ROUNDS),
|
||||
tokenHash: await bcrypt.hash(token, getSaltRounds()),
|
||||
expiresAt
|
||||
}
|
||||
|
||||
|
@ -1,28 +1,183 @@
|
||||
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');
|
||||
|
||||
DatabaseService.initDatabase(MONGO_URL);
|
||||
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';
|
||||
|
||||
setUpHealthEndpoint(server);
|
||||
const main = async () => {
|
||||
if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) {
|
||||
await infisical.connect({
|
||||
token: process.env.INFISICAL_TOKEN!
|
||||
});
|
||||
}
|
||||
|
||||
setTransporter(initSmtp());
|
||||
logTelemetryMessage();
|
||||
setTransporter(initSmtp());
|
||||
|
||||
if (NODE_ENV !== 'test') {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
debug: NODE_ENV === 'production' ? false : true,
|
||||
environment: NODE_ENV
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
createTestUserForDevelopment()
|
||||
export default main();
|
@ -10,11 +10,13 @@ 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,
|
||||
@ -23,26 +25,30 @@ 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 {
|
||||
@ -77,6 +83,12 @@ const getApps = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
apps = await getAppsGitlab({
|
||||
accessToken,
|
||||
teamId
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
apps = await getAppsRender({
|
||||
accessToken,
|
||||
@ -190,21 +202,40 @@ const getAppsVercel = async ({
|
||||
* @returns {String} apps.name - name of Netlify site
|
||||
*/
|
||||
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
const apps: any = [];
|
||||
try {
|
||||
const res = (
|
||||
await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
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,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
})
|
||||
).data;
|
||||
});
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.site_id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
appId: a.site_id,
|
||||
}));
|
||||
page++;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@ -217,9 +248,9 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
/**
|
||||
* Return list of repositories for Github integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Netlify API
|
||||
* @returns {Object[]} apps - names of Netlify sites
|
||||
* @returns {String} apps.name - name of Netlify site
|
||||
* @param {String} obj.accessToken - access token for Github API
|
||||
* @returns {Object[]} apps - names of Github sites
|
||||
* @returns {String} apps.name - name of Github site
|
||||
*/
|
||||
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
@ -401,4 +432,117 @@ 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,28 +1,32 @@
|
||||
import request from '../config/request';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import request from '../config/request';
|
||||
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_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL
|
||||
} from '../variables';
|
||||
import {
|
||||
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
|
||||
getSiteURL,
|
||||
getClientIdAzure,
|
||||
getClientSecretAzure,
|
||||
getClientSecretHeroku,
|
||||
getClientIdVercel,
|
||||
getClientSecretVercel,
|
||||
getClientIdNetlify,
|
||||
getClientSecretNetlify,
|
||||
getClientIdGitHub,
|
||||
getClientSecretGitHub,
|
||||
getClientIdGitLab,
|
||||
getClientSecretGitLab
|
||||
} from '../config';
|
||||
|
||||
interface ExchangeCodeAzureResponse {
|
||||
@ -66,6 +70,15 @@ 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]
|
||||
@ -114,6 +127,10 @@ const exchangeCode = async ({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
obj = await exchangeCodeGitlab({
|
||||
code
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@ -142,16 +159,16 @@ const exchangeCodeAzure = async ({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
scope: 'https://vault.azure.net/.default openid offline_access',
|
||||
client_id: CLIENT_ID_AZURE,
|
||||
client_secret: CLIENT_SECRET_AZURE,
|
||||
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||
client_id: getClientIdAzure(),
|
||||
client_secret: getClientSecretAzure(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||
@ -187,7 +204,7 @@ const exchangeCodeHeroku = async ({
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
client_secret: getClientSecretHeroku()
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
@ -225,9 +242,9 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
code: code,
|
||||
client_id: CLIENT_ID_VERCEL,
|
||||
client_secret: CLIENT_SECRET_VERCEL,
|
||||
redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
|
||||
client_id: getClientIdVercel(),
|
||||
client_secret: getClientSecretVercel(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
@ -265,9 +282,9 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: CLIENT_ID_NETLIFY,
|
||||
client_secret: CLIENT_SECRET_NETLIFY,
|
||||
redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
|
||||
client_id: getClientIdNetlify(),
|
||||
client_secret: getClientSecretNetlify(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
@ -316,10 +333,10 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||
res = (
|
||||
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
|
||||
params: {
|
||||
client_id: CLIENT_ID_GITHUB,
|
||||
client_secret: CLIENT_SECRET_GITHUB,
|
||||
client_id: getClientIdGitHub(),
|
||||
client_secret: getClientSecretGitHub(),
|
||||
code: code,
|
||||
redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
|
||||
redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback`
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
@ -341,4 +358,53 @@ 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,6 +1,7 @@
|
||||
import { exchangeCode } from './exchange';
|
||||
import { exchangeRefresh } from './refresh';
|
||||
import { getApps } from './apps';
|
||||
import { getTeams } from './teams';
|
||||
import { syncSecrets } from './sync';
|
||||
import { revokeAccess } from './revoke';
|
||||
|
||||
@ -8,6 +9,7 @@ export {
|
||||
exchangeCode,
|
||||
exchangeRefresh,
|
||||
getApps,
|
||||
getTeams,
|
||||
syncSecrets,
|
||||
revokeAccess
|
||||
}
|
@ -1,16 +1,29 @@
|
||||
import request from '../config/request';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
|
||||
import request from '../config/request';
|
||||
import {
|
||||
SITE_URL,
|
||||
CLIENT_ID_AZURE,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU
|
||||
} from '../config';
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_GITLAB,
|
||||
} from '../variables';
|
||||
import {
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL
|
||||
} from '../variables';
|
||||
import {
|
||||
IntegrationService
|
||||
} from '../services';
|
||||
import {
|
||||
getSiteURL,
|
||||
getClientIdAzure,
|
||||
getClientSecretAzure,
|
||||
getClientSecretHeroku,
|
||||
getClientIdGitLab,
|
||||
getClientSecretGitLab
|
||||
} from '../config';
|
||||
|
||||
interface RefreshTokenAzureResponse {
|
||||
token_type: string;
|
||||
@ -21,6 +34,23 @@ 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]
|
||||
@ -29,33 +59,61 @@ interface RefreshTokenAzureResponse {
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||
*/
|
||||
const exchangeRefresh = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
refreshToken
|
||||
}: {
|
||||
integration: string;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
|
||||
interface TokenDetails {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}
|
||||
|
||||
let tokenDetails: TokenDetails;
|
||||
try {
|
||||
switch (integration) {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
accessToken = await exchangeRefreshAzure({
|
||||
tokenDetails = await exchangeRefreshAzure({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
accessToken = await exchangeRefreshHeroku({
|
||||
tokenDetails = 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;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -71,18 +129,27 @@ const exchangeRefreshAzure = async ({
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const res: RefreshTokenAzureResponse = (await request.post(
|
||||
const accessExpiresAt = new Date();
|
||||
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: CLIENT_ID_AZURE,
|
||||
client_id: getClientIdAzure(),
|
||||
scope: 'openid offline_access',
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
client_secret: CLIENT_SECRET_AZURE
|
||||
client_secret: getClientSecretAzure()
|
||||
} as any)
|
||||
)).data;
|
||||
);
|
||||
|
||||
return res.access_token;
|
||||
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);
|
||||
@ -102,26 +169,84 @@ const exchangeRefreshHeroku = async ({
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
let accessToken;
|
||||
try {
|
||||
const res = await request.post(
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenHerokuResponse
|
||||
} = await request.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
client_secret: getClientSecretHeroku()
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
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 Heroku');
|
||||
}
|
||||
};
|
||||
|
||||
return accessToken;
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
};
|
||||
|
||||
export { exchangeRefresh };
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
} from '../variables';
|
||||
|
||||
const revokeAccess = async ({
|
||||
@ -32,6 +33,8 @@ const revokeAccess = async ({
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
break;
|
||||
}
|
||||
|
||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
|
@ -19,11 +19,13 @@ 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,
|
||||
@ -110,6 +112,13 @@ const syncSecrets = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
await syncSecretsGitLab({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
await syncSecretsRender({
|
||||
integration,
|
||||
@ -163,7 +172,6 @@ const syncSecretsAzureKeyVault = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
|
||||
interface GetAzureKeyVaultSecret {
|
||||
id: string; // secret URI
|
||||
attributes: {
|
||||
@ -186,17 +194,22 @@ const syncSecretsAzureKeyVault = async ({
|
||||
*/
|
||||
const paginateAzureKeyVaultSecrets = async (url: string) => {
|
||||
let result: GetAzureKeyVaultSecret[] = [];
|
||||
|
||||
while (url) {
|
||||
const res = await request.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
try {
|
||||
while (url) {
|
||||
const res = await request.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
result = result.concat(res.data.value);
|
||||
|
||||
url = res.data.nextLink;
|
||||
}
|
||||
|
||||
result = result.concat(res.data.value);
|
||||
url = res.data.nextLink;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -212,8 +225,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
|
||||
const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
@ -259,33 +271,75 @@ 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
|
||||
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 setSecret of setSecrets) {
|
||||
const { key, value } = setSecret;
|
||||
setSecretAzureKeyVault({
|
||||
key,
|
||||
value,
|
||||
integration,
|
||||
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'
|
||||
}
|
||||
});
|
||||
for await (const deleteSecret of deleteSecrets) {
|
||||
const { key } = deleteSecret;
|
||||
await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -1422,7 +1476,98 @@ const syncSecretsTravisCI = async ({
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to sync secrets to TravisCI");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
92
backend/src/integrations/teams.ts
Normal file
92
backend/src/integrations/teams.ts
Normal file
@ -0,0 +1,92 @@
|
||||
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,16 +1,13 @@
|
||||
import { ErrorRequestHandler } from "express";
|
||||
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { InternalServerError, UnauthorizedRequestError, UnprocessableEntityError } from "../utils/errors";
|
||||
import { ErrorRequestHandler } from "express";
|
||||
import { InternalServerError } from "../utils/errors";
|
||||
import { getLogger } from "../utils/logger";
|
||||
import RequestError, { LogLevel } from "../utils/requestError";
|
||||
import { NODE_ENV } from "../config";
|
||||
|
||||
import mongoose from "mongoose";
|
||||
import { getNodeEnv } from '../config';
|
||||
|
||||
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
if (NODE_ENV !== "production") {
|
||||
if (getNodeEnv() !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.log(error)
|
||||
/* eslint-enable no-console */
|
||||
@ -34,17 +31,4 @@ 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,12 +1,14 @@
|
||||
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 {
|
||||
@ -25,9 +27,11 @@ declare module 'jsonwebtoken' {
|
||||
* @returns
|
||||
*/
|
||||
const requireAuth = ({
|
||||
acceptedAuthModes = ['jwt']
|
||||
acceptedAuthModes = ['jwt'],
|
||||
requiredServiceTokenPermissions = []
|
||||
}: {
|
||||
acceptedAuthModes: string[];
|
||||
requiredServiceTokenPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||
@ -38,11 +42,22 @@ const requireAuth = ({
|
||||
});
|
||||
|
||||
// attach auth payloads
|
||||
let serviceTokenData: any;
|
||||
switch (authTokenType) {
|
||||
case 'serviceToken':
|
||||
req.serviceTokenData = await getAuthSTDPayload({
|
||||
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, JWT_MFA_SECRET)
|
||||
jwt.verify(AUTH_TOKEN_VALUE, getJwtMfaSecret())
|
||||
);
|
||||
|
||||
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, JWT_SERVICE_SECRET)
|
||||
jwt.verify(AUTH_TOKEN_VALUE, getJwtServiceSecret())
|
||||
);
|
||||
|
||||
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, JWT_SIGNUP_SECRET)
|
||||
jwt.verify(AUTH_TOKEN_VALUE, getJwtSignupSecret())
|
||||
);
|
||||
|
||||
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 !== workspaceId
|
||||
&& req.serviceTokenData.environment !== req.query.environment
|
||||
&& req.serviceTokenData.workspace.toString() !== workspaceId
|
||||
&& req.serviceTokenData.environment !== req.body.environment
|
||||
) {
|
||||
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@ -31,7 +32,8 @@ export interface IIntegration {
|
||||
| 'heroku'
|
||||
| 'vercel'
|
||||
| 'netlify'
|
||||
| 'github'
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'render'
|
||||
| 'flyio'
|
||||
| 'circleci'
|
||||
@ -60,13 +62,11 @@ 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,6 +96,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@ -16,15 +17,15 @@ import {
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | '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; // new
|
||||
accessIdIV?: string; // new
|
||||
accessIdTag?: string; // new
|
||||
accessIdCiphertext?: string;
|
||||
accessIdIV?: string;
|
||||
accessIdTag?: string;
|
||||
accessCiphertext?: string;
|
||||
accessIV?: string;
|
||||
accessTag?: string;
|
||||
@ -48,6 +49,7 @@ 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[];
|
||||
}
|
||||
|
||||
export const secretSchema = new Schema<ISecret>(
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
{
|
||||
version: {
|
||||
type: Number,
|
||||
|
@ -1,28 +1,18 @@
|
||||
import mongoose, { Schema, model } from 'mongoose';
|
||||
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
|
||||
}
|
||||
import Secret, { ISecret } from './secret';
|
||||
|
||||
interface ISecretApprovalRequest {
|
||||
environment: string;
|
||||
workspace: mongoose.Types.ObjectId;
|
||||
requestedChanges: IRequestedChange[];
|
||||
requestedByUserId: mongoose.Types.ObjectId;
|
||||
secret: mongoose.Types.ObjectId;
|
||||
requestedChanges: ISecret;
|
||||
requestedBy: mongoose.Types.ObjectId;
|
||||
approvers: IApprover[];
|
||||
status: ApprovalStatus;
|
||||
timestamp: Date;
|
||||
requestType: ChangeType;
|
||||
requestType: RequestType;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface IApprover {
|
||||
interface IApprover {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
}
|
||||
@ -33,80 +23,54 @@ export enum ApprovalStatus {
|
||||
REJECTED = 'rejected'
|
||||
}
|
||||
|
||||
export enum ChangeType {
|
||||
export enum RequestType {
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
CREATE = 'create'
|
||||
}
|
||||
|
||||
const approverSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: false,
|
||||
required: true
|
||||
},
|
||||
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,
|
||||
});
|
||||
|
||||
SecretModificationSchema.remove("version")
|
||||
|
||||
|
||||
const requestedChangeSchema = new mongoose.Schema(
|
||||
{
|
||||
_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'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ChangeType,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ApprovalStatus,
|
||||
default: ApprovalStatus.PENDING // the overall status of the requested change
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
merged: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
{
|
||||
environment: {
|
||||
type: String, // The secret changes were requested for
|
||||
secret: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Secret'
|
||||
},
|
||||
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: {
|
||||
requestedChanges: Secret,
|
||||
requestedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ApprovalStatus,
|
||||
default: ApprovalStatus.PENDING
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
requestType: {
|
||||
type: String,
|
||||
enum: RequestType,
|
||||
required: true
|
||||
},
|
||||
requestId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -114,8 +78,6 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
}
|
||||
);
|
||||
|
||||
secretApprovalRequestSchema.index({ 'requestedChanges.approvers.userId': 1 });
|
||||
|
||||
const SecretApprovalRequest = model<ISecretApprovalRequest>('secret_approval_request', secretApprovalRequestSchema);
|
||||
const SecretApprovalRequest = model<ISecretApprovalRequest>('SecretApprovalRequest', secretApprovalRequestSchema);
|
||||
|
||||
export default SecretApprovalRequest;
|
||||
|
@ -3,13 +3,14 @@ import { Schema, model, Types } from 'mongoose';
|
||||
export interface IServiceTokenData {
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string; // TODO: adapt to upcoming environment id
|
||||
environment: string;
|
||||
user: Types.ObjectId;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
@ -51,6 +52,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
tag: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: ['read', 'write'],
|
||||
default: ['read']
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||
|
||||
export interface IToken {
|
||||
email: string;
|
||||
|
@ -1,16 +1,9 @@
|
||||
import mongoose, { Schema, model, Types } from 'mongoose';
|
||||
|
||||
|
||||
export interface DesignatedApprovers {
|
||||
environment: string,
|
||||
approvers: [mongoose.Schema.Types.ObjectId]
|
||||
}
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IWorkspace {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
organization: Types.ObjectId;
|
||||
approvers: [DesignatedApprovers];
|
||||
environments: Array<{
|
||||
name: string;
|
||||
slug: string;
|
||||
@ -18,16 +11,6 @@ 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,
|
||||
@ -37,7 +20,6 @@ const workspaceSchema = new Schema<IWorkspace>({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { getSmtpConfigured } from '../../config';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@ -8,6 +9,7 @@ router.get(
|
||||
res.status(200).json({
|
||||
date: new Date(),
|
||||
message: 'Ok',
|
||||
emailConfigured: getSmtpConfigured()
|
||||
})
|
||||
}
|
||||
);
|
||||
|
@ -15,7 +15,6 @@ import password from './password';
|
||||
import stripe from './stripe';
|
||||
import integration from './integration';
|
||||
import integrationAuth from './integrationAuth';
|
||||
import secretApprovalRequest from './secretApprovalsRequest'
|
||||
|
||||
export {
|
||||
signup,
|
||||
@ -34,6 +33,5 @@ export {
|
||||
password,
|
||||
stripe,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secretApprovalRequest
|
||||
integrationAuth
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
@ -73,10 +73,24 @@ 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({
|
||||
|
@ -1,65 +0,0 @@
|
||||
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,34 +134,6 @@ 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,7 +22,8 @@ import {
|
||||
router.post(
|
||||
'/batch',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['read', 'write']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@ -99,7 +100,8 @@ router.post(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@ -115,7 +117,8 @@ router.get(
|
||||
query('tagSlugs'),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['read']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@ -154,7 +157,8 @@ router.patch(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@ -182,7 +186,8 @@ router.delete(
|
||||
.isEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@ -192,5 +197,3 @@ router.delete(
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
|
@ -30,13 +30,22 @@ router.post(
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('name').exists().trim(),
|
||||
body('workspaceId'),
|
||||
body('environment'),
|
||||
body('encryptedKey'),
|
||||
body('iv'),
|
||||
body('tag'),
|
||||
body('expiresIn'), // measured in ms
|
||||
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
|
||||
}),
|
||||
validateRequest,
|
||||
serviceTokenDataController.createServiceTokenData
|
||||
);
|
||||
|
@ -1,16 +1,32 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { getLogger } from '../utils/logger';
|
||||
import { initDatabaseHelper } from '../helpers/database';
|
||||
import {
|
||||
initDatabaseHelper,
|
||||
closeDatabaseHelper
|
||||
} 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,9 +7,6 @@ import {
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
|
||||
/**
|
||||
* Class to handle integrations
|
||||
*/
|
||||
|
@ -1,27 +1,44 @@
|
||||
import { PostHog } from 'posthog-node';
|
||||
import {
|
||||
NODE_ENV,
|
||||
POSTHOG_HOST,
|
||||
POSTHOG_PROJECT_API_KEY,
|
||||
TELEMETRY_ENABLED
|
||||
} from '../config';
|
||||
import { getLogger } from '../utils/logger';
|
||||
import {
|
||||
getNodeEnv,
|
||||
getTelemetryEnabled,
|
||||
getPostHogProjectApiKey,
|
||||
getPostHogHost
|
||||
} from '../config';
|
||||
|
||||
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'))
|
||||
/**
|
||||
* 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'))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
export default postHogClient;
|
||||
|
@ -1,13 +1,14 @@
|
||||
import DatabaseService from './DatabaseService';
|
||||
import postHogClient from './PostHogClient';
|
||||
import { logTelemetryMessage, getPostHogClient } 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,55 +1,68 @@
|
||||
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_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
} 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
|
||||
export const initSmtp = () => {
|
||||
const mailOpts: SMTPConnection.Options = {
|
||||
host: getSmtpHost(),
|
||||
port: getSmtpPort()
|
||||
};
|
||||
}
|
||||
|
||||
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')) {
|
||||
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'
|
||||
}
|
||||
} else {
|
||||
mailOpts.secure = true;
|
||||
}
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initSmtp = () => {
|
||||
const transporter = nodemailer.createTransport(mailOpts);
|
||||
transporter
|
||||
.verify()
|
||||
@ -60,7 +73,7 @@ export const initSmtp = () => {
|
||||
.catch((err) => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(
|
||||
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
|
||||
`SMTP - Failed to connect to ${getSmtpHost()}:${getSmtpPort()} \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 (NODE_ENV === "development") {
|
||||
if (getNodeEnv() === "development") {
|
||||
const testUserEmail = "test@localhost.local"
|
||||
const testUserPassword = "testInfisical1"
|
||||
const testUserId = "63cefa6ec8d3175601cfa980"
|
||||
|
@ -19,15 +19,6 @@ 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,
|
||||
@ -36,7 +27,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,
|
||||
@ -55,15 +46,6 @@ 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 { LOKI_HOST, NODE_ENV } from '../config';
|
||||
import { getLokiHost, getNodeEnv } 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(LOKI_HOST !== undefined){
|
||||
if(getLokiHost() !== undefined){
|
||||
_transports.push(
|
||||
new LokiTransport({
|
||||
host: LOKI_HOST,
|
||||
host: getLokiHost(),
|
||||
handleExceptions: true,
|
||||
handleRejections: true,
|
||||
batching: true,
|
||||
@ -37,7 +37,11 @@ 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: NODE_ENV},
|
||||
labels: {
|
||||
app: process.env.npm_package_name,
|
||||
version: process.env.npm_package_version,
|
||||
environment: getNodeEnv()
|
||||
},
|
||||
onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err)
|
||||
})
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Request } from 'express'
|
||||
import { VERBOSE_ERROR_OUTPUT } from '../config'
|
||||
import { getVerboseErrorOutput } 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(!VERBOSE_ERROR_OUTPUT){
|
||||
if(!getVerboseErrorOutput()){
|
||||
_context = this._omit(_context, [
|
||||
'stacktrace',
|
||||
'exception',
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@ -24,14 +25,16 @@ 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,
|
||||
INTEGRATION_OPTIONS,
|
||||
getIntegrationOptions
|
||||
} from "./integration";
|
||||
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
|
||||
@ -47,7 +50,8 @@ import {
|
||||
import {
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_SOCKETLABS
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
} from './smtp';
|
||||
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
||||
import {
|
||||
@ -80,6 +84,7 @@ export {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@ -91,7 +96,9 @@ 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,
|
||||
@ -106,10 +113,11 @@ export {
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
INTEGRATION_OPTIONS,
|
||||
getIntegrationOptions,
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
MFA_METHOD_EMAIL,
|
||||
|
@ -1,13 +1,11 @@
|
||||
import {
|
||||
CLIENT_ID_AZURE,
|
||||
TENANT_ID_AZURE
|
||||
getClientIdHeroku,
|
||||
getClientSlugVercel,
|
||||
getClientIdNetlify,
|
||||
getClientIdAzure,
|
||||
getClientIdGitLab,
|
||||
getClientIdGitHub
|
||||
} 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';
|
||||
@ -17,6 +15,7 @@ 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";
|
||||
@ -27,6 +26,7 @@ const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@ -37,16 +37,18 @@ const INTEGRATION_SET = new Set([
|
||||
const INTEGRATION_OAUTH2 = "oauth2";
|
||||
|
||||
// integration oauth endpoints
|
||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/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";
|
||||
@ -54,144 +56,160 @@ 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 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: ''
|
||||
}
|
||||
]
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
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_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,
|
||||
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
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
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_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
}
|
@ -197,6 +197,23 @@ 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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
5
backend/tests/example.test.ts
Normal file
5
backend/tests/example.test.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { it, expect } from '@jest/globals';
|
||||
|
||||
it('should return true', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
21
backend/tests/setupTests.ts
Normal file
21
backend/tests/setupTests.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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);
|
||||
});
|
||||
});
|
@ -7,7 +7,7 @@ require (
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.6.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/term v0.5.0
|
||||
)
|
||||
|
||||
@ -31,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.6.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
)
|
||||
|
||||
|
16
cli/go.sum
16
cli/go.sum
@ -103,17 +103,13 @@ 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 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.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=
|
||||
@ -125,13 +121,9 @@ 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,6 +49,11 @@ 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)
|
||||
@ -69,7 +74,7 @@ var exportCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId})
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to fetch secrets")
|
||||
}
|
||||
@ -106,6 +111,7 @@ 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,7 +6,6 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
@ -96,7 +95,7 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, os.ModePerm)
|
||||
err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -41,13 +41,14 @@ var loginCmd = &cobra.Command{
|
||||
PreRun: toggleDebug,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||
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
|
||||
// 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")) {
|
||||
log.Debug(err)
|
||||
} else if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired { // if you are logged in but not expired
|
||||
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
|
||||
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
@ -234,8 +235,16 @@ var loginCmd = &cobra.Command{
|
||||
// clear backed up secrets from prev account
|
||||
util.DeleteBackupSecrets()
|
||||
|
||||
color.Green("Nice! You are logged in as: %v", email)
|
||||
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)
|
||||
|
||||
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,6 +36,9 @@ var resetCmd = &cobra.Command{
|
||||
|
||||
keyringInstance.Remove(util.KEYRING_SERVICE_NAME)
|
||||
|
||||
// delete secrets backup
|
||||
util.DeleteBackupSecrets()
|
||||
|
||||
util.PrintSuccessMessage("Reset successful")
|
||||
},
|
||||
}
|
||||
|
@ -186,14 +186,11 @@ 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 {
|
||||
shell := subShellCmd()
|
||||
command := args[0]
|
||||
argsForCommand := args[1:]
|
||||
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
|
||||
|
||||
args = append(args[:1], args[0:]...) // shift args to the right
|
||||
args[0] = shell[1]
|
||||
|
||||
cmd := exec.Command(shell[0], args...)
|
||||
|
||||
cmd := exec.Command(command, argsForCommand...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@ -203,7 +200,15 @@ func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string)
|
||||
}
|
||||
|
||||
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
|
||||
shell := subShellCmd()
|
||||
shell := [2]string{"sh", "-c"}
|
||||
if runtime.GOOS == "windows" {
|
||||
shell = [2]string{"cmd", "/C"}
|
||||
} else {
|
||||
currentShell := os.Getenv("SHELL")
|
||||
if currentShell != "" {
|
||||
shell[0] = currentShell
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell[0], shell[1], fullCommand)
|
||||
cmd.Stdin = os.Stdin
|
||||
@ -217,23 +222,6 @@ 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,6 +19,7 @@ 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"
|
||||
)
|
||||
|
||||
@ -132,6 +133,11 @@ 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)
|
||||
|
||||
|
@ -55,4 +55,5 @@ type GetAllSecretsParameters struct {
|
||||
EnvironmentPassedViaFlag bool
|
||||
InfisicalToken string
|
||||
TagSlugs string
|
||||
WorkspaceId string
|
||||
}
|
||||
|
@ -4,18 +4,47 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"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 {
|
||||
PrintWarning(fmt.Sprintf("Please update your CLI. You are running version %s but the latest version is %s", CLI_VERSION, latestVersion))
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,12 +55,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 := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -40,7 +69,59 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
json.Unmarshal(body, &tags)
|
||||
if err := json.Unmarshal(body, &tags); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal github response: %w", err)
|
||||
}
|
||||
|
||||
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, os.ModePerm)
|
||||
err = WriteToFile(fullConfigFilePath, configFileMarshalled, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -151,52 +151,6 @@ 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()
|
||||
@ -243,7 +197,7 @@ func WriteConfigFile(configFile *models.ConfigFile) error {
|
||||
}
|
||||
|
||||
// Create file in directory
|
||||
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
|
||||
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err)
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ 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)
|
||||
}
|
||||
|
||||
|
@ -76,10 +76,31 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
||||
return nil, fmt.Errorf("unable to get your encrypted workspace key. [err=%v]", err)
|
||||
}
|
||||
|
||||
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
|
||||
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
|
||||
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
|
||||
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(receiversPrivateKey)
|
||||
encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKey")
|
||||
}
|
||||
|
||||
encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey")
|
||||
}
|
||||
|
||||
encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce")
|
||||
}
|
||||
|
||||
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiversPrivateKey)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for currentUsersPrivateKey")
|
||||
}
|
||||
|
||||
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
|
||||
log.Debugf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
|
||||
PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
|
||||
}
|
||||
|
||||
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
||||
@ -131,6 +152,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params.WorkspaceId != "" {
|
||||
workspaceFile.WorkspaceId = params.WorkspaceId
|
||||
}
|
||||
|
||||
// Verify environment
|
||||
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
|
||||
if err != nil {
|
||||
@ -415,7 +440,7 @@ func WriteBackupSecrets(workspace string, environment string, encryptionKey []by
|
||||
}
|
||||
|
||||
listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, os.ModePerm)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
|
||||
}
|
||||
|
176
cloudformation/ec2-deployment/infisical-ec2-deployment.template
Normal file
176
cloudformation/ec2-deployment/infisical-ec2-deployment.template
Normal file
@ -0,0 +1,176 @@
|
||||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Description: >-
|
||||
CloudFormation template to deploy Infisical on a EC2 instance with a
|
||||
DocumentDB instance
|
||||
Parameters:
|
||||
KeyPairName:
|
||||
Description: The name of the EC2 Key Pair to enable SSH access to the instance
|
||||
Type: "AWS::EC2::KeyPair::KeyName"
|
||||
VpcId:
|
||||
Description: The ID of the VPC in which to launch the instance
|
||||
Type: "AWS::EC2::VPC::Id"
|
||||
DocumentDBUsername:
|
||||
Description: The username for the DocumentDB instance
|
||||
Type: String
|
||||
MinLength: 5
|
||||
DocumentDBPassword:
|
||||
Description: The password for the DocumentDB instance (minimum 8 characters)
|
||||
Type: String
|
||||
MinLength: 8
|
||||
NoEcho: true
|
||||
Resources:
|
||||
DocumentDBCluster:
|
||||
Type: "AWS::DocDB::DBCluster"
|
||||
Properties:
|
||||
EngineVersion: 4.0.0
|
||||
StorageEncrypted: true
|
||||
MasterUsername: !Ref DocumentDBUsername
|
||||
MasterUserPassword: !Ref DocumentDBPassword
|
||||
VpcSecurityGroupIds:
|
||||
- !Ref DocumentDBClusterSecurityGroup
|
||||
DBClusterParameterGroupName: !Ref DBClusterParameterGroup
|
||||
Metadata:
|
||||
"AWS::CloudFormation::Designer":
|
||||
id: 73b974cf-eed3-4f7d-8657-6a6746bac169
|
||||
DependsOn:
|
||||
- DBClusterParameterGroup
|
||||
DBClusterParameterGroup:
|
||||
Type: "AWS::DocDB::DBClusterParameterGroup"
|
||||
Properties:
|
||||
Description: "description"
|
||||
Family: "docdb4.0"
|
||||
Parameters:
|
||||
tls: "disabled"
|
||||
ttl_monitor: "disabled"
|
||||
Tags:
|
||||
- Key: "String"
|
||||
Value: "String"
|
||||
DocumentDBInstance:
|
||||
Type: "AWS::DocDB::DBInstance"
|
||||
Properties:
|
||||
DBInstanceClass: db.t4g.medium
|
||||
DBClusterIdentifier: !Ref DocumentDBCluster
|
||||
Metadata:
|
||||
"AWS::CloudFormation::Designer":
|
||||
id: f04cee38-175e-4432-9ad7-62ca28bbf935
|
||||
DocumentDBClusterSecurityGroup:
|
||||
Type: AWS::EC2::SecurityGroup
|
||||
Properties:
|
||||
GroupDescription: Allow inbound traffic for DocumentDB cluster
|
||||
VpcId: !Ref VpcId
|
||||
SecurityGroupIngress:
|
||||
- IpProtocol: tcp
|
||||
FromPort: 27017
|
||||
ToPort: 27017
|
||||
SourceSecurityGroupId: !Ref InstanceSecurityGroup
|
||||
EC2Instance:
|
||||
Type: "AWS::EC2::Instance"
|
||||
Properties:
|
||||
ImageId: ami-0557a15b87f6559cf
|
||||
InstanceType: t2.medium
|
||||
KeyName: !Ref KeyPairName
|
||||
UserData:
|
||||
Fn::Base64: !Sub |
|
||||
#!/bin/bash
|
||||
cd /home/ubuntu
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
git clone https://github.com/Infisical/infisical.git
|
||||
cd infisical
|
||||
|
||||
DOCUMENT_DB_CONNECTION_URL="mongodb://${DocumentDBUsername}:${DocumentDBPassword}@${DocumentDBCluster.Endpoint}:${DocumentDBCluster.Port}/infisical?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false"
|
||||
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 16)
|
||||
JWT_SIGNUP_SECRET=$(openssl rand -hex 16)
|
||||
JWT_REFRESH_SECRET=$(openssl rand -hex 16)
|
||||
JWT_AUTH_SECRET=$(openssl rand -hex 16)
|
||||
JWT_SERVICE_SECRET=$(openssl rand -hex 16)
|
||||
|
||||
touch .env
|
||||
|
||||
echo "ENCRYPTION_KEY=${!ENCRYPTION_KEY}" >> .env
|
||||
echo "JWT_SIGNUP_SECRET=${!JWT_SIGNUP_SECRET}" >> .env
|
||||
echo "JWT_REFRESH_SECRET=${!JWT_REFRESH_SECRET}" >> .env
|
||||
echo "JWT_AUTH_SECRET=${!JWT_AUTH_SECRET}" >> .env
|
||||
echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env
|
||||
echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
SecurityGroupIds:
|
||||
- !Ref InstanceSecurityGroup
|
||||
Tags:
|
||||
- Key: Name
|
||||
Value: infisical
|
||||
Metadata:
|
||||
"AWS::CloudFormation::Designer":
|
||||
id: 2c0a771c-5002-4785-9848-0377e33cd0e9
|
||||
DependsOn:
|
||||
- DocumentDBInstance
|
||||
|
||||
InstanceSecurityGroup:
|
||||
Type: "AWS::EC2::SecurityGroup"
|
||||
Properties:
|
||||
GroupDescription: Allow SSH and HTTP traffic
|
||||
SecurityGroupIngress:
|
||||
- IpProtocol: tcp
|
||||
FromPort: 22
|
||||
ToPort: 22
|
||||
CidrIp: 0.0.0.0/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 80
|
||||
ToPort: 80
|
||||
CidrIp: 0.0.0.0/0
|
||||
VpcId: !Ref VpcId
|
||||
Metadata:
|
||||
"AWS::CloudFormation::Designer":
|
||||
id: 1fd6856a-11e5-4369-84fa-d18d4011b3de
|
||||
|
||||
Outputs:
|
||||
InstanceIP:
|
||||
Value: !GetAtt EC2Instance.PublicIp
|
||||
Metadata:
|
||||
"AWS::CloudFormation::Designer":
|
||||
1fd6856a-11e5-4369-84fa-d18d4011b3de:
|
||||
size:
|
||||
width: 60
|
||||
height: 60
|
||||
position:
|
||||
x: 60
|
||||
"y": 90
|
||||
z: 1
|
||||
embeds: []
|
||||
2c0a771c-5002-4785-9848-0377e33cd0e9:
|
||||
size:
|
||||
width: 60
|
||||
height: 60
|
||||
position:
|
||||
x: 180
|
||||
"y": 90
|
||||
z: 1
|
||||
embeds: []
|
||||
isassociatedwith:
|
||||
- 1fd6856a-11e5-4369-84fa-d18d4011b3de
|
||||
dependson:
|
||||
- 2cabaada-fbdb-4945-bf95-a0406704dd5a
|
||||
- f04cee38-175e-4432-9ad7-62ca28bbf935
|
||||
73b974cf-eed3-4f7d-8657-6a6746bac169:
|
||||
size:
|
||||
width: 60
|
||||
height: 60
|
||||
position:
|
||||
x: 390
|
||||
"y": 210
|
||||
z: 1
|
||||
embeds: []
|
||||
f04cee38-175e-4432-9ad7-62ca28bbf935:
|
||||
size:
|
||||
width: 60
|
||||
height: 60
|
||||
position:
|
||||
x: 270
|
||||
"y": 90
|
||||
z: 1
|
||||
embeds: []
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user