mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
Merge branch 'feature/google-signin-signup-integration' of https://github.com/sheensantoscapadngan/infisical into feature/google-signin-signup-integration
This commit is contained in:
@@ -13,6 +13,7 @@ jobs:
|
|||||||
check-be-pr:
|
check-be-pr:
|
||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
@@ -26,17 +27,17 @@ jobs:
|
|||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
run: npm ci --only-production
|
run: npm ci --only-production
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: 🧪 Run tests
|
# - name: 🧪 Run tests
|
||||||
run: npm run test:ci
|
# run: npm run test:ci
|
||||||
working-directory: backend
|
# working-directory: backend
|
||||||
- name: 📁 Upload test results
|
# - name: 📁 Upload test results
|
||||||
uses: actions/upload-artifact@v3
|
# uses: actions/upload-artifact@v3
|
||||||
if: always()
|
# if: always()
|
||||||
with:
|
# with:
|
||||||
name: be-test-results
|
# name: be-test-results
|
||||||
path: |
|
# path: |
|
||||||
./backend/reports
|
# ./backend/reports
|
||||||
./backend/coverage
|
# ./backend/coverage
|
||||||
- name: 🏗️ Run build
|
- name: 🏗️ Run build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
|||||||
@@ -2,40 +2,35 @@ name: Check Frontend Pull Request
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize ]
|
types: [opened, synchronize]
|
||||||
paths:
|
paths:
|
||||||
- 'frontend/**'
|
- "frontend/**"
|
||||||
- '!frontend/README.md'
|
- "!frontend/README.md"
|
||||||
- '!frontend/.*'
|
- "!frontend/.*"
|
||||||
- 'frontend/.eslintrc.js'
|
- "frontend/.eslintrc.js"
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
check-fe-pr:
|
check-fe-pr:
|
||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: ☁️ Checkout source
|
||||||
name: ☁️ Checkout source
|
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
-
|
- name: 🔧 Setup Node 16
|
||||||
name: 🔧 Setup Node 16
|
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: "16"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
-
|
- name: 📦 Install dependencies
|
||||||
name: 📦 Install dependencies
|
|
||||||
run: npm ci --only-production --ignore-scripts
|
run: npm ci --only-production --ignore-scripts
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
# -
|
# -
|
||||||
# name: 🧪 Run tests
|
# name: 🧪 Run tests
|
||||||
# run: npm run test:ci
|
# run: npm run test:ci
|
||||||
# working-directory: frontend
|
# working-directory: frontend
|
||||||
-
|
- name: 🏗️ Run build
|
||||||
name: 🏗️ Run build
|
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://cloudsmith.io/~infisical/repos/">
|
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||||
<img src="https://img.shields.io/badge/Downloads-150.8k-orange" alt="Cloudsmith downloads" />
|
<img src="https://img.shields.io/badge/Downloads-240.2k-orange" alt="Cloudsmith downloads" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
<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" />
|
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||||
@@ -55,7 +55,7 @@ We're on a mission to make secret management more accessible to everyone, not ju
|
|||||||
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
|
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
|
||||||
- **Role-based Access Controls** per environment
|
- **Role-based Access Controls** per environment
|
||||||
- [**Simple on-premise deployments** to AWS and Digital Ocean](https://infisical.com/docs/self-hosting/overview)
|
- [**Simple on-premise deployments** to AWS and Digital Ocean](https://infisical.com/docs/self-hosting/overview)
|
||||||
- [**2FA**](https://infisical.com/docs/documentation/platform/mfa) with more options coming soon
|
- [**Secret Scanning**](https://infisical.com/docs/cli/scanning-overview)
|
||||||
|
|
||||||
And much more.
|
And much more.
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -33,7 +33,7 @@
|
|||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
"infisical-node": "^1.1.3",
|
"infisical-node": "^1.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
"infisical-node": "^1.1.3",
|
"infisical-node": "^1.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import InfisicalClient from 'infisical-node';
|
import InfisicalClient from 'infisical-node';
|
||||||
|
|
||||||
const client = new InfisicalClient({
|
export const client = new InfisicalClient({
|
||||||
token: process.env.INFISICAL_TOKEN!
|
token: process.env.INFISICAL_TOKEN!
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
|
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
|
||||||
|
export const getEncryptionKey = async () => {
|
||||||
|
const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue;
|
||||||
|
return secretValue === '' ? undefined : secretValue;
|
||||||
|
}
|
||||||
|
export const getRootEncryptionKey = async () => {
|
||||||
|
const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
|
||||||
|
return secretValue === '' ? undefined : secretValue;
|
||||||
|
}
|
||||||
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
|
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
|
||||||
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue;
|
|
||||||
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
|
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
|
||||||
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
|
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
|
||||||
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
|
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
|
||||||
@@ -50,8 +57,14 @@ export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWOR
|
|||||||
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
|
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
|
||||||
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
|
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
|
||||||
|
|
||||||
export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue;
|
export const getLicenseKey = async () => {
|
||||||
export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
|
const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue;
|
||||||
|
return secretValue === '' ? undefined : secretValue;
|
||||||
|
}
|
||||||
|
export const getLicenseServerKey = async () => {
|
||||||
|
const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
|
||||||
|
return secretValue === '' ? undefined : secretValue;
|
||||||
|
}
|
||||||
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
|
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
|
||||||
|
|
||||||
// TODO: deprecate from here
|
// TODO: deprecate from here
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
Bot
|
Bot
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
|
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
|
||||||
import { IntegrationService } from '../../services';
|
import { IntegrationService } from '../../services';
|
||||||
import {
|
import {
|
||||||
getApps,
|
getApps,
|
||||||
@@ -129,7 +129,9 @@ export const saveIntegrationAccessToken = async (
|
|||||||
integration
|
integration
|
||||||
}, {
|
}, {
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
integration
|
integration,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}, {
|
}, {
|
||||||
new: true,
|
new: true,
|
||||||
upsert: true
|
upsert: true
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
|||||||
error: 'Failed to send email verification code'
|
error: 'Failed to send email verification code'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: `Sent an email verification code to ${email}`
|
message: `Sent an email verification code to ${email}`
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre
|
|||||||
const { ValidationError } = mongoose.Error;
|
const { ValidationError } = mongoose.Error;
|
||||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||||
import { AnyBulkWriteOperation } from 'mongodb';
|
import { AnyBulkWriteOperation } from 'mongodb';
|
||||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||||
import { TelemetryService } from '../../services';
|
import { TelemetryService } from '../../services';
|
||||||
import { User } from "../../models";
|
import { User } from "../../models";
|
||||||
import { AccountNotFoundError } from '../../utils/errors';
|
import { AccountNotFoundError } from '../../utils/errors';
|
||||||
@@ -36,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => {
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: secretToCreate.type,
|
type: secretToCreate.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id),
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +94,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: rawSecret.type,
|
type: rawSecret.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id),
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitizedSecretesToCreate.push(safeUpdateFields)
|
sanitizedSecretesToCreate.push(safeUpdateFields)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import {
|
|||||||
ACTION_ADD_SECRETS,
|
ACTION_ADD_SECRETS,
|
||||||
ACTION_READ_SECRETS,
|
ACTION_READ_SECRETS,
|
||||||
ACTION_UPDATE_SECRETS,
|
ACTION_UPDATE_SECRETS,
|
||||||
ACTION_DELETE_SECRETS
|
ACTION_DELETE_SECRETS,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors';
|
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors';
|
||||||
import { EventService } from '../../services';
|
import { EventService } from '../../services';
|
||||||
@@ -51,7 +53,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
if (!workspace) throw WorkspaceNotFoundError();
|
if (!workspace) throw WorkspaceNotFoundError();
|
||||||
|
|
||||||
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
||||||
const isPaid = orgPlan.tier < 1;
|
const isPaid = orgPlan.tier >= 1;
|
||||||
|
|
||||||
const createSecrets: BatchSecret[] = [];
|
const createSecrets: BatchSecret[] = [];
|
||||||
const updateSecrets: BatchSecret[] = [];
|
const updateSecrets: BatchSecret[] = [];
|
||||||
@@ -85,7 +87,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
path: fullFolderPath,
|
path: fullFolderPath,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
secretBlindIndex
|
secretBlindIndex,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'PATCH':
|
case 'PATCH':
|
||||||
@@ -100,6 +104,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
path: fullFolderPath,
|
path: fullFolderPath,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'DELETE':
|
case 'DELETE':
|
||||||
@@ -144,7 +150,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: req.user.email
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,6 +208,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentCiphertext: u.secretCommentCiphertext,
|
secretCommentCiphertext: u.secretCommentCiphertext,
|
||||||
secretCommentIV: u.secretCommentIV,
|
secretCommentIV: u.secretCommentIV,
|
||||||
secretCommentTag: u.secretCommentTag,
|
secretCommentTag: u.secretCommentTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags: u.tags
|
tags: u.tags
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -232,7 +241,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: req.user.email
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -268,7 +278,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: req.user.email
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -387,7 +398,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
if (!workspace) throw WorkspaceNotFoundError();
|
if (!workspace) throw WorkspaceNotFoundError();
|
||||||
|
|
||||||
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
||||||
const isPaid = orgPlan.tier < 1;
|
const isPaid = orgPlan.tier >= 1;
|
||||||
|
|
||||||
let listOfSecretsToCreate;
|
let listOfSecretsToCreate;
|
||||||
if (Array.isArray(req.body.secrets)) {
|
if (Array.isArray(req.body.secrets)) {
|
||||||
@@ -457,6 +468,8 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags
|
tags
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -503,7 +516,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag
|
secretValueTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -545,7 +560,10 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: await TelemetryService.getDistinctId({
|
||||||
|
authData: req.authData
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -613,7 +631,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
if (!workspace) throw WorkspaceNotFoundError();
|
if (!workspace) throw WorkspaceNotFoundError();
|
||||||
|
|
||||||
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
||||||
const isPaid = orgPlan.tier < 1;
|
const isPaid = orgPlan.tier >= 1;
|
||||||
|
|
||||||
// secrets to return
|
// secrets to return
|
||||||
let secrets: ISecret[] = [];
|
let secrets: ISecret[] = [];
|
||||||
@@ -748,7 +766,10 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: await TelemetryService.getDistinctId({
|
||||||
|
authData: req.authData
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -852,6 +873,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags,
|
tags,
|
||||||
...((
|
...((
|
||||||
secretCommentCiphertext !== undefined &&
|
secretCommentCiphertext !== undefined &&
|
||||||
@@ -905,6 +928,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
|
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
|
||||||
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||||
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags: tags ? tags : secret.tags
|
tags: tags ? tags : secret.tags
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -963,7 +988,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
if (!workspace) throw WorkspaceNotFoundError();
|
if (!workspace) throw WorkspaceNotFoundError();
|
||||||
|
|
||||||
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
||||||
const isPaid = orgPlan.tier < 1;
|
const isPaid = orgPlan.tier >= 1;
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
@@ -978,7 +1003,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: key,
|
workspaceId: key,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: await TelemetryService.getDistinctId({
|
||||||
|
authData: req.authData
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1100,13 +1128,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(key)
|
workspaceId: new Types.ObjectId(key)
|
||||||
});
|
});
|
||||||
|
|
||||||
const organizationId = (
|
const workspace = await Workspace.findById(key);
|
||||||
await Workspace.findOne({
|
if (!workspace) throw WorkspaceNotFoundError();
|
||||||
_id: key
|
|
||||||
})
|
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
|
||||||
)?.organization?.toString();
|
const isPaid = orgPlan.tier >= 1;
|
||||||
const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || '');
|
|
||||||
const isPaid = orgPlan.slug != 'starter';
|
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
@@ -1121,7 +1147,10 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: key,
|
workspaceId: key,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.['user-agent'],
|
userAgent: req.headers?.['user-agent'],
|
||||||
isPaid
|
isPaid,
|
||||||
|
email: await TelemetryService.getDistinctId({
|
||||||
|
authData: req.authData
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import { EELicenseService } from '../../services';
|
|||||||
* Return the organization's current plan and allowed feature set
|
* Return the organization's current plan and allowed feature set
|
||||||
*/
|
*/
|
||||||
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
||||||
const plan = await EELicenseService.getOrganizationPlan(req.organization._id.toString());
|
const { organizationId } = req.params;
|
||||||
|
|
||||||
// cache fetched plan for organization
|
const plan = await EELicenseService.getOrganizationPlan(organizationId);
|
||||||
EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
plan
|
plan,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding
|
||||||
} = oldSecretVersion;
|
} = oldSecretVersion;
|
||||||
|
|
||||||
// update secret
|
// update secret
|
||||||
@@ -182,6 +184,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
@@ -205,7 +209,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
|
|||||||
@@ -93,52 +93,8 @@ const markDeletedSecretVersionsHelper = async ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize secret versioning by setting previously unversioned
|
|
||||||
* secrets to version 1 and begin populating secret versions.
|
|
||||||
*/
|
|
||||||
const initSecretVersioningHelper = async () => {
|
|
||||||
await Secret.updateMany(
|
|
||||||
{ version: { $exists: false } },
|
|
||||||
{ $set: { version: 1 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: "secretversions",
|
|
||||||
localField: "_id",
|
|
||||||
foreignField: "secret",
|
|
||||||
as: "versions",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
versions: { $size: 0 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (unversionedSecrets.length > 0) {
|
|
||||||
await addSecretVersionsHelper({
|
|
||||||
secretVersions: unversionedSecrets.map(
|
|
||||||
(s, idx) =>
|
|
||||||
new SecretVersion({
|
|
||||||
...s,
|
|
||||||
secret: s._id,
|
|
||||||
version: s.version ? s.version : 1,
|
|
||||||
isDeleted: false,
|
|
||||||
workspace: s.workspace,
|
|
||||||
environment: s.environment,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper,
|
markDeletedSecretVersionsHelper
|
||||||
initSecretVersioningHelper,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
|
|
||||||
export interface ISecretVersion {
|
export interface ISecretVersion {
|
||||||
@@ -20,6 +23,8 @@ export interface ISecretVersion {
|
|||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
|
algorithm: 'aes-256-gcm';
|
||||||
|
keyEncoding: 'utf8' | 'base64';
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||||
@@ -85,7 +90,20 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
secretValueTag: {
|
secretValueTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
|
algorithm: { // the encryption algorithm used
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ import { OrganizationNotFoundError } from '../../utils/errors';
|
|||||||
interface FeatureSet {
|
interface FeatureSet {
|
||||||
_id: string | null;
|
_id: string | null;
|
||||||
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
|
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
|
||||||
tier: number | null;
|
tier: number;
|
||||||
projectLimit: number | null;
|
workspaceLimit: number | null;
|
||||||
|
workspacesUsed: number;
|
||||||
memberLimit: number | null;
|
memberLimit: number | null;
|
||||||
|
membersUsed: number;
|
||||||
secretVersioning: boolean;
|
secretVersioning: boolean;
|
||||||
pitRecovery: boolean;
|
pitRecovery: boolean;
|
||||||
rbac: boolean;
|
rbac: boolean;
|
||||||
@@ -44,8 +46,10 @@ class EELicenseService {
|
|||||||
_id: null,
|
_id: null,
|
||||||
slug: null,
|
slug: null,
|
||||||
tier: -1,
|
tier: -1,
|
||||||
projectLimit: null,
|
workspaceLimit: null,
|
||||||
|
workspacesUsed: 0,
|
||||||
memberLimit: null,
|
memberLimit: null,
|
||||||
|
membersUsed: 0,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: true,
|
pitRecovery: true,
|
||||||
rbac: true,
|
rbac: true,
|
||||||
@@ -63,11 +67,13 @@ class EELicenseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOrganizationPlan(organizationId: string) {
|
public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> {
|
||||||
try {
|
try {
|
||||||
if (this.instanceType === 'cloud') {
|
if (this.instanceType === 'cloud') {
|
||||||
const cachedPlan = this.localFeatureSet.get(organizationId);
|
const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId);
|
||||||
if (cachedPlan) return cachedPlan;
|
if (cachedPlan) {
|
||||||
|
return cachedPlan;
|
||||||
|
}
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
const organization = await Organization.findById(organizationId);
|
||||||
if (!organization) throw OrganizationNotFoundError();
|
if (!organization) throw OrganizationNotFoundError();
|
||||||
@@ -76,6 +82,9 @@ class EELicenseService {
|
|||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// cache fetched plan for organization
|
||||||
|
this.localFeatureSet.set(organizationId, currentPlan);
|
||||||
|
|
||||||
return currentPlan;
|
return currentPlan;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { ISecretVersion } from '../models';
|
|||||||
import {
|
import {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper,
|
markDeletedSecretVersionsHelper
|
||||||
initSecretVersioningHelper
|
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import EELicenseService from './EELicenseService';
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
@@ -64,15 +63,6 @@ class EESecretService {
|
|||||||
secretIds
|
secretIds
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize secret versioning by setting previously unversioned
|
|
||||||
* secrets to version 1 and begin populating secret versions.
|
|
||||||
*/
|
|
||||||
static async initSecretVersioning() {
|
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
|
||||||
await initSecretVersioningHelper();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EESecretService;
|
export default EESecretService;
|
||||||
@@ -42,7 +42,6 @@ const validateAuthMode = ({
|
|||||||
headers: { [key: string]: string | string[] | undefined },
|
headers: { [key: string]: string | string[] | undefined },
|
||||||
acceptedAuthModes: string[]
|
acceptedAuthModes: string[]
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: refactor middleware
|
|
||||||
const apiKey = headers['x-api-key'];
|
const apiKey = headers['x-api-key'];
|
||||||
const authHeader = headers['authorization'];
|
const authHeader = headers['authorization'];
|
||||||
|
|
||||||
|
|||||||
+98
-131
@@ -4,107 +4,26 @@ import {
|
|||||||
BotKey,
|
BotKey,
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
IUser,
|
IUser
|
||||||
User,
|
|
||||||
IServiceAccount,
|
|
||||||
ServiceAccount,
|
|
||||||
IServiceTokenData,
|
|
||||||
ServiceTokenData,
|
|
||||||
} from "../models";
|
} from "../models";
|
||||||
import {
|
import {
|
||||||
generateKeyPair,
|
generateKeyPair,
|
||||||
encryptSymmetric,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
decryptSymmetric,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric
|
||||||
} from "../utils/crypto";
|
} from '../utils/crypto';
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
AUTH_MODE_JWT,
|
ALGORITHM_AES_256_GCM,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
ENCODING_SCHEME_UTF8,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
ENCODING_SCHEME_BASE64
|
||||||
AUTH_MODE_API_KEY,
|
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import { getEncryptionKey } from "../config";
|
import {
|
||||||
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors";
|
getEncryptionKey,
|
||||||
import { validateMembership } from "../helpers/membership";
|
getRootEncryptionKey,
|
||||||
import { validateUserClientForWorkspace } from "../helpers/user";
|
client
|
||||||
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount";
|
} from "../config";
|
||||||
|
import { InternalServerError } from "../utils/errors";
|
||||||
/**
|
|
||||||
* Validate authenticated clients for bot with id [botId] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.botId - id of bot to validate against
|
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
|
||||||
*/
|
|
||||||
const validateClientForBot = async ({
|
|
||||||
authData,
|
|
||||||
botId,
|
|
||||||
acceptedRoles,
|
|
||||||
}: {
|
|
||||||
authData: {
|
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
botId: Types.ObjectId;
|
|
||||||
acceptedRoles: Array<"admin" | "member">;
|
|
||||||
}) => {
|
|
||||||
const bot = await Bot.findById(botId);
|
|
||||||
|
|
||||||
if (!bot) throw BotNotFoundError();
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_JWT &&
|
|
||||||
authData.authPayload instanceof User
|
|
||||||
) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: bot.workspace,
|
|
||||||
acceptedRoles,
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
|
||||||
authData.authPayload instanceof ServiceAccount
|
|
||||||
) {
|
|
||||||
await validateServiceAccountClientForWorkspace({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
workspaceId: bot.workspace,
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
|
||||||
authData.authPayload instanceof ServiceTokenData
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed service token authorization for bot",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_API_KEY &&
|
|
||||||
authData.authPayload instanceof User
|
|
||||||
) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: bot.workspace,
|
|
||||||
acceptedRoles,
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw BotNotFoundError({
|
|
||||||
message: "Failed client authorization for bot",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
@@ -119,23 +38,52 @@ const createBot = async ({
|
|||||||
name: string;
|
name: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const { publicKey, privateKey } = generateKeyPair();
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
|
||||||
plaintext: privateKey,
|
if (rootEncryptionKey) {
|
||||||
key: await getEncryptionKey(),
|
const {
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
return await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
} else if (encryptionKey) {
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: await getEncryptionKey(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: 'Failed to create new bot due to missing encryption key'
|
||||||
});
|
});
|
||||||
|
|
||||||
const bot = await new Bot({
|
|
||||||
name,
|
|
||||||
workspace: workspaceId,
|
|
||||||
isActive: false,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,14 +109,14 @@ const getSecretsHelper = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
secrets.forEach((secret: ISecret) => {
|
secrets.forEach((secret: ISecret) => {
|
||||||
const secretKey = decryptSymmetric({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
tag: secret.secretKeyTag,
|
tag: secret.secretKeyTag,
|
||||||
key,
|
key,
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretValueCiphertext,
|
ciphertext: secret.secretValueCiphertext,
|
||||||
iv: secret.secretValueIV,
|
iv: secret.secretValueIV,
|
||||||
tag: secret.secretValueTag,
|
tag: secret.secretValueTag,
|
||||||
@@ -189,34 +137,54 @@ const getSecretsHelper = async ({
|
|||||||
* @returns {String} key - decrypted workspace key
|
* @returns {String} key - decrypted workspace key
|
||||||
*/
|
*/
|
||||||
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const botKey = await BotKey.findOne({
|
const botKey = await BotKey.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).populate<{ sender: IUser }>("sender", "publicKey");
|
})
|
||||||
|
.populate<{ sender: IUser }>("sender", "publicKey");
|
||||||
|
|
||||||
if (!botKey) throw new Error("Failed to find bot key");
|
if (!botKey) throw new Error("Failed to find bot key");
|
||||||
|
|
||||||
const bot = await Bot.findOne({
|
const bot = await Bot.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).select("+encryptedPrivateKey +iv +tag");
|
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
|
||||||
|
|
||||||
if (!bot) throw new Error("Failed to find bot");
|
if (!bot) throw new Error("Failed to find bot");
|
||||||
if (!bot.isActive) throw new Error("Bot is not active");
|
if (!bot.isActive) throw new Error("Bot is not active");
|
||||||
|
|
||||||
const privateKeyBot = decryptSymmetric({
|
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
// case: encoding scheme is base64
|
||||||
iv: bot.iv,
|
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
|
||||||
tag: bot.tag,
|
|
||||||
key: await getEncryptionKey(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const key = decryptAsymmetric({
|
return decryptAsymmetric({
|
||||||
ciphertext: botKey.encryptedKey,
|
ciphertext: botKey.encryptedKey,
|
||||||
nonce: botKey.nonce,
|
nonce: botKey.nonce,
|
||||||
publicKey: botKey.sender.publicKey as string,
|
publicKey: botKey.sender.publicKey as string,
|
||||||
privateKey: privateKeyBot,
|
privateKey: privateKeyBot,
|
||||||
});
|
});
|
||||||
|
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||||
|
|
||||||
|
// case: encoding scheme is utf8
|
||||||
|
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
publicKey: botKey.sender.publicKey as string,
|
||||||
|
privateKey: privateKeyBot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return key;
|
throw InternalServerError({
|
||||||
|
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,7 +202,7 @@ const encryptSymmetricHelper = async ({
|
|||||||
plaintext: string;
|
plaintext: string;
|
||||||
}) => {
|
}) => {
|
||||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext,
|
plaintext,
|
||||||
key,
|
key,
|
||||||
});
|
});
|
||||||
@@ -266,7 +234,7 @@ const decryptSymmetricHelper = async ({
|
|||||||
tag: string;
|
tag: string;
|
||||||
}) => {
|
}) => {
|
||||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||||
const plaintext = decryptSymmetric({
|
const plaintext = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext,
|
ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
@@ -277,9 +245,8 @@ const decryptSymmetricHelper = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForBot,
|
|
||||||
createBot,
|
createBot,
|
||||||
getSecretsHelper,
|
getSecretsHelper,
|
||||||
encryptSymmetricHelper,
|
encryptSymmetricHelper,
|
||||||
decryptSymmetricHelper,
|
decryptSymmetricHelper
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { EESecretService } from '../ee/services';
|
|
||||||
import { SecretService } from '../services';
|
|
||||||
import { getLogger } from '../utils/logger';
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,9 +19,7 @@ const initDatabaseHelper = async ({
|
|||||||
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
|
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
|
||||||
|
|
||||||
(await getLogger("database")).info("Database connection established");
|
(await getLogger("database")).info("Database connection established");
|
||||||
|
|
||||||
await EESecretService.initSecretVersioning();
|
|
||||||
await SecretService.initSecretBlindIndexDataHelper();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
|
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,40 +3,20 @@ import { Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth
|
||||||
IUser,
|
|
||||||
User,
|
|
||||||
IServiceAccount,
|
|
||||||
ServiceAccount,
|
|
||||||
IServiceTokenData,
|
|
||||||
ServiceTokenData
|
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
import { BotService } from '../services';
|
import { BotService } from '../services';
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY,
|
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY
|
INTEGRATION_NETLIFY,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
import {
|
||||||
UnauthorizedRequestError,
|
UnauthorizedRequestError,
|
||||||
IntegrationAuthNotFoundError,
|
|
||||||
IntegrationNotFoundError
|
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import RequestError from '../utils/requestError';
|
import RequestError from '../utils/requestError';
|
||||||
import {
|
|
||||||
validateClientForIntegrationAuth
|
|
||||||
} from '../helpers/integrationAuth';
|
|
||||||
import {
|
|
||||||
validateUserClientForWorkspace
|
|
||||||
} from '../helpers/user';
|
|
||||||
import {
|
|
||||||
validateServiceAccountClientForWorkspace
|
|
||||||
} from '../helpers/serviceAccount';
|
|
||||||
import { IntegrationService } from '../services';
|
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
workspace: string;
|
workspace: string;
|
||||||
@@ -45,84 +25,6 @@ interface Update {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate authenticated clients for integration with id [integrationId] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.integrationId - id of integration to validate against
|
|
||||||
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
|
||||||
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateClientForIntegration = async ({
|
|
||||||
authData,
|
|
||||||
integrationId,
|
|
||||||
acceptedRoles
|
|
||||||
}: {
|
|
||||||
authData: {
|
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
integrationId: Types.ObjectId;
|
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const integration = await Integration.findById(integrationId);
|
|
||||||
if (!integration) throw IntegrationNotFoundError();
|
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth
|
|
||||||
.findById(integration.integrationAuth)
|
|
||||||
.select(
|
|
||||||
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!integrationAuth) throw IntegrationAuthNotFoundError();
|
|
||||||
|
|
||||||
const accessToken = (await IntegrationService.getIntegrationAuthAccess({
|
|
||||||
integrationAuthId: integrationAuth._id
|
|
||||||
})).accessToken;
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: integration.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
return ({ integration, accessToken });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
|
||||||
await validateServiceAccountClientForWorkspace({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
workspaceId: integration.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
return ({ integration, accessToken });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed service token authorization for integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: integration.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
return ({ integration, accessToken });
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
* named [integration]
|
* named [integration]
|
||||||
@@ -400,7 +302,9 @@ const setIntegrationAuthRefreshHelper = async ({
|
|||||||
}, {
|
}, {
|
||||||
refreshCiphertext: obj.ciphertext,
|
refreshCiphertext: obj.ciphertext,
|
||||||
refreshIV: obj.iv,
|
refreshIV: obj.iv,
|
||||||
refreshTag: obj.tag
|
refreshTag: obj.tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}, {
|
}, {
|
||||||
new: true
|
new: true
|
||||||
});
|
});
|
||||||
@@ -461,7 +365,9 @@ const setIntegrationAuthAccessHelper = async ({
|
|||||||
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
||||||
accessIV: encryptedAccessTokenObj.iv,
|
accessIV: encryptedAccessTokenObj.iv,
|
||||||
accessTag: encryptedAccessTokenObj.tag,
|
accessTag: encryptedAccessTokenObj.tag,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}, {
|
}, {
|
||||||
new: true
|
new: true
|
||||||
});
|
});
|
||||||
@@ -475,7 +381,6 @@ const setIntegrationAuthAccessHelper = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForIntegration,
|
|
||||||
handleOAuthExchangeHelper,
|
handleOAuthExchangeHelper,
|
||||||
syncIntegrationsHelper,
|
syncIntegrationsHelper,
|
||||||
getIntegrationAuthRefreshHelper,
|
getIntegrationAuthRefreshHelper,
|
||||||
|
|||||||
@@ -2,105 +2,12 @@ import * as Sentry from '@sentry/node';
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key
|
||||||
IUser,
|
|
||||||
User,
|
|
||||||
IServiceAccount,
|
|
||||||
ServiceAccount,
|
|
||||||
IServiceTokenData,
|
|
||||||
ServiceTokenData
|
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
MembershipNotFoundError,
|
MembershipNotFoundError,
|
||||||
BadRequestError,
|
BadRequestError
|
||||||
UnauthorizedRequestError
|
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY
|
|
||||||
} from '../variables';
|
|
||||||
import {
|
|
||||||
validateUserClientForWorkspace
|
|
||||||
} from '../helpers/user';
|
|
||||||
import {
|
|
||||||
validateServiceAccountClientForWorkspace
|
|
||||||
} from '../helpers/serviceAccount';
|
|
||||||
import {
|
|
||||||
validateServiceTokenDataClientForWorkspace
|
|
||||||
} from '../helpers/serviceTokenData';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate authenticated clients for membership with id [membershipId] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
|
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
|
|
||||||
* @returns {Membership} - validated membership
|
|
||||||
*/
|
|
||||||
const validateClientForMembership = async ({
|
|
||||||
authData,
|
|
||||||
membershipId,
|
|
||||||
acceptedRoles
|
|
||||||
}: {
|
|
||||||
authData: {
|
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
membershipId: Types.ObjectId;
|
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const membership = await Membership.findById(membershipId);
|
|
||||||
|
|
||||||
if (!membership) throw MembershipNotFoundError({
|
|
||||||
message: 'Failed to find membership'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: membership.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
|
||||||
await validateServiceAccountClientForWorkspace({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
workspaceId: membership.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
await validateServiceTokenDataClientForWorkspace({
|
|
||||||
serviceTokenData: authData.authPayload,
|
|
||||||
workspaceId: new Types.ObjectId(membership.workspace)
|
|
||||||
});
|
|
||||||
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: membership.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
@@ -230,7 +137,6 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForMembership,
|
|
||||||
validateMembership,
|
validateMembership,
|
||||||
addMemberships,
|
addMemberships,
|
||||||
findMembership,
|
findMembership,
|
||||||
|
|||||||
@@ -3,95 +3,12 @@ import {
|
|||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
Workspace,
|
Workspace,
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key
|
||||||
IUser,
|
|
||||||
User,
|
|
||||||
IServiceAccount,
|
|
||||||
ServiceAccount,
|
|
||||||
IServiceTokenData,
|
|
||||||
ServiceTokenData
|
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
MembershipOrgNotFoundError,
|
MembershipOrgNotFoundError,
|
||||||
BadRequestError,
|
|
||||||
UnauthorizedRequestError
|
UnauthorizedRequestError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY
|
|
||||||
} from '../variables';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate authenticated clients for organization membership with id [membershipOrgId] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
|
|
||||||
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
|
|
||||||
* @param {MembershipOrg} - validated organization membership
|
|
||||||
*/
|
|
||||||
const validateClientForMembershipOrg = async ({
|
|
||||||
authData,
|
|
||||||
membershipOrgId,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses
|
|
||||||
}: {
|
|
||||||
authData: {
|
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
membershipOrgId: Types.ObjectId;
|
|
||||||
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
|
||||||
acceptedStatuses: Array<'invited' | 'accepted'>;
|
|
||||||
}) => {
|
|
||||||
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
|
|
||||||
|
|
||||||
if (!membershipOrg) throw MembershipOrgNotFoundError({
|
|
||||||
message: 'Failed to find organization membership '
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
await validateMembershipOrg({
|
|
||||||
userId: authData.authPayload._id,
|
|
||||||
organizationId: membershipOrg.organization,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses
|
|
||||||
});
|
|
||||||
|
|
||||||
return membershipOrg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
|
||||||
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed service account client authorization for organization membership'
|
|
||||||
});
|
|
||||||
|
|
||||||
return membershipOrg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed service account client authorization for organization membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateMembershipOrg({
|
|
||||||
userId: authData.authPayload._id,
|
|
||||||
organizationId: membershipOrg.organization,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses
|
|
||||||
});
|
|
||||||
|
|
||||||
return membershipOrg;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for organization membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||||
@@ -234,7 +151,6 @@ const deleteMembershipOrg = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForMembershipOrg,
|
|
||||||
validateMembershipOrg,
|
validateMembershipOrg,
|
||||||
findMembershipOrg,
|
findMembershipOrg,
|
||||||
addMembershipsOrg,
|
addMembershipsOrg,
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
|
||||||
IUser,
|
|
||||||
User,
|
|
||||||
IServiceAccount,
|
|
||||||
ServiceAccount,
|
|
||||||
IServiceTokenData,
|
|
||||||
ServiceTokenData,
|
|
||||||
} from "../models";
|
|
||||||
import { Organization, MembershipOrg } from "../models";
|
import { Organization, MembershipOrg } from "../models";
|
||||||
import {
|
import {
|
||||||
ACCEPTED,
|
ACCEPTED
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY,
|
|
||||||
OWNER,
|
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import {
|
import {
|
||||||
getStripeSecretKey,
|
getStripeSecretKey,
|
||||||
@@ -23,12 +10,6 @@ import {
|
|||||||
getStripeProductTeam,
|
getStripeProductTeam,
|
||||||
getStripeProductStarter,
|
getStripeProductStarter,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import {
|
|
||||||
UnauthorizedRequestError,
|
|
||||||
OrganizationNotFoundError,
|
|
||||||
} from "../utils/errors";
|
|
||||||
import { validateUserClientForOrganization } from "../helpers/user";
|
|
||||||
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
|
|
||||||
import {
|
import {
|
||||||
EELicenseService
|
EELicenseService
|
||||||
} from '../ee/services';
|
} from '../ee/services';
|
||||||
@@ -40,88 +21,6 @@ import {
|
|||||||
licenseKeyRequest
|
licenseKeyRequest
|
||||||
} from '../config/request';
|
} from '../config/request';
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate accepted clients for organization with id [organizationId]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
|
|
||||||
*/
|
|
||||||
const validateClientForOrganization = async ({
|
|
||||||
authData,
|
|
||||||
organizationId,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses,
|
|
||||||
}: {
|
|
||||||
authData: {
|
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
organizationId: Types.ObjectId;
|
|
||||||
acceptedRoles: Array<"owner" | "admin" | "member">;
|
|
||||||
acceptedStatuses: Array<"invited" | "accepted">;
|
|
||||||
}) => {
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_JWT &&
|
|
||||||
authData.authPayload instanceof User
|
|
||||||
) {
|
|
||||||
const membershipOrg = await validateUserClientForOrganization({
|
|
||||||
user: authData.authPayload,
|
|
||||||
organization,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { organization, membershipOrg };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
|
||||||
authData.authPayload instanceof ServiceAccount
|
|
||||||
) {
|
|
||||||
await validateServiceAccountClientForOrganization({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { organization };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
|
||||||
authData.authPayload instanceof ServiceTokenData
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed service token authorization for organization",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.authMode === AUTH_MODE_API_KEY &&
|
|
||||||
authData.authPayload instanceof User
|
|
||||||
) {
|
|
||||||
const membershipOrg = await validateUserClientForOrganization({
|
|
||||||
user: authData.authPayload,
|
|
||||||
organization,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { organization, membershipOrg };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed client authorization for organization",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an organization with name [name]
|
* Create an organization with name [name]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -251,6 +150,8 @@ const updateSubscriptionOrgQuantity = async ({
|
|||||||
quantity
|
quantity
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
EELicenseService.localFeatureSet.del(organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
|
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
|
||||||
@@ -273,8 +174,7 @@ const updateSubscriptionOrgQuantity = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForOrganization,
|
|
||||||
createOrganization,
|
createOrganization,
|
||||||
initSubscriptionOrg,
|
initSubscriptionOrg,
|
||||||
updateSubscriptionOrgQuantity,
|
updateSubscriptionOrgQuantity
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
ACTION_UPDATE_SECRETS,
|
ACTION_UPDATE_SECRETS,
|
||||||
ACTION_DELETE_SECRETS,
|
ACTION_DELETE_SECRETS,
|
||||||
ACTION_READ_SECRETS,
|
ACTION_READ_SECRETS,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
||||||
@@ -194,6 +196,8 @@ const v1PushSecrets = async ({
|
|||||||
secretValueIV: newSecret.ivValue,
|
secretValueIV: newSecret.ivValue,
|
||||||
secretValueTag: newSecret.tagValue,
|
secretValueTag: newSecret.tagValue,
|
||||||
secretValueHash: newSecret.hashValue,
|
secretValueHash: newSecret.hashValue,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -225,6 +229,8 @@ const v1PushSecrets = async ({
|
|||||||
secretCommentIV: s.ivComment,
|
secretCommentIV: s.ivComment,
|
||||||
secretCommentTag: s.tagComment,
|
secretCommentTag: s.tagComment,
|
||||||
secretCommentHash: s.hashComment,
|
secretCommentHash: s.hashComment,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
};
|
};
|
||||||
|
|
||||||
if (toAdd[idx].type === "personal") {
|
if (toAdd[idx].type === "personal") {
|
||||||
@@ -254,6 +260,8 @@ const v1PushSecrets = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
secretValueHash,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding
|
||||||
}) =>
|
}) =>
|
||||||
new SecretVersion({
|
new SecretVersion({
|
||||||
secret: _id,
|
secret: _id,
|
||||||
@@ -271,6 +279,8 @@ const v1PushSecrets = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
secretValueHash,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -467,6 +477,8 @@ const v2PushSecrets = async ({
|
|||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
type: toAdd[idx].type,
|
type: toAdd[idx].type,
|
||||||
environment,
|
environment,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
...(toAdd[idx].type === "personal" ? { user: userId } : {}),
|
...(toAdd[idx].type === "personal" ? { user: userId } : {}),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -478,6 +490,8 @@ const v2PushSecrets = async ({
|
|||||||
...secretDocument,
|
...secretDocument,
|
||||||
secret: secretDocument._id,
|
secret: secretDocument._id,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+84
-262
@@ -7,58 +7,35 @@ import {
|
|||||||
DeleteSecretParams
|
DeleteSecretParams
|
||||||
} from '../interfaces/services/SecretService';
|
} from '../interfaces/services/SecretService';
|
||||||
import {
|
import {
|
||||||
AuthData
|
|
||||||
} from '../interfaces/middleware';
|
|
||||||
import {
|
|
||||||
User,
|
|
||||||
Workspace,
|
|
||||||
ServiceAccount,
|
|
||||||
ServiceTokenData,
|
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { SecretVersion } from '../ee/models';
|
import { SecretVersion } from '../ee/models';
|
||||||
import {
|
|
||||||
validateMembership
|
|
||||||
} from '../helpers/membership';
|
|
||||||
import {
|
|
||||||
validateUserClientForSecret,
|
|
||||||
validateUserClientForSecrets
|
|
||||||
} from '../helpers/user';
|
|
||||||
import {
|
|
||||||
validateServiceTokenDataClientForSecrets,
|
|
||||||
validateServiceTokenDataClientForWorkspace
|
|
||||||
} from '../helpers/serviceTokenData';
|
|
||||||
import {
|
|
||||||
validateServiceAccountClientForSecrets,
|
|
||||||
validateServiceAccountClientForWorkspace
|
|
||||||
} from '../helpers/serviceAccount';
|
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
UnauthorizedRequestError,
|
|
||||||
SecretNotFoundError,
|
SecretNotFoundError,
|
||||||
SecretBlindIndexDataNotFoundError
|
SecretBlindIndexDataNotFoundError,
|
||||||
|
InternalServerError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY,
|
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
ACTION_ADD_SECRETS,
|
ACTION_ADD_SECRETS,
|
||||||
ACTION_READ_SECRETS,
|
ACTION_READ_SECRETS,
|
||||||
ACTION_UPDATE_SECRETS,
|
ACTION_UPDATE_SECRETS,
|
||||||
ACTION_DELETE_SECRETS
|
ACTION_DELETE_SECRETS,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import {
|
import {
|
||||||
encryptSymmetric,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
decryptSymmetric
|
decryptSymmetric128BitHexKeyUTF8
|
||||||
} from '../utils/crypto';
|
} from '../utils/crypto';
|
||||||
import { getEncryptionKey } from '../config';
|
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
|
||||||
import { TelemetryService } from '../services';
|
import { TelemetryService } from '../services';
|
||||||
import {
|
import {
|
||||||
EESecretService,
|
EESecretService,
|
||||||
@@ -69,199 +46,6 @@ import {
|
|||||||
getAuthDataPayloadUserObj
|
getAuthDataPayloadUserObj
|
||||||
} from '../utils/auth';
|
} from '../utils/auth';
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate authenticated clients for secrets with id [secretId] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
|
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
|
||||||
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateClientForSecret = async ({
|
|
||||||
authData,
|
|
||||||
secretId,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
}: {
|
|
||||||
authData: AuthData;
|
|
||||||
secretId: Types.ObjectId;
|
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
|
||||||
requiredPermissions: string[];
|
|
||||||
}) => {
|
|
||||||
const secret = await Secret.findById(secretId);
|
|
||||||
|
|
||||||
if (!secret) throw SecretNotFoundError({
|
|
||||||
message: 'Failed to find secret'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForSecret({
|
|
||||||
user: authData.authPayload,
|
|
||||||
secret,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
|
||||||
await validateServiceAccountClientForWorkspace({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
workspaceId: secret.workspace,
|
|
||||||
environment: secret.environment,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
await validateServiceTokenDataClientForWorkspace({
|
|
||||||
serviceTokenData: authData.authPayload,
|
|
||||||
workspaceId: secret.workspace,
|
|
||||||
environment: secret.environment
|
|
||||||
});
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForSecret({
|
|
||||||
user: authData.authPayload,
|
|
||||||
secret,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for secret'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate authenticated clients for secrets with ids [secretIds] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
|
|
||||||
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
|
||||||
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateClientForSecrets = async ({
|
|
||||||
authData,
|
|
||||||
secretIds,
|
|
||||||
requiredPermissions
|
|
||||||
}: {
|
|
||||||
authData: AuthData;
|
|
||||||
secretIds: Types.ObjectId[];
|
|
||||||
requiredPermissions: string[];
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
let secrets: ISecret[] = [];
|
|
||||||
|
|
||||||
secrets = await Secret.find({
|
|
||||||
_id: {
|
|
||||||
$in: secretIds
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (secrets.length != secretIds.length) {
|
|
||||||
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForSecrets({
|
|
||||||
user: authData.authPayload,
|
|
||||||
secrets,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
|
||||||
await validateServiceAccountClientForSecrets({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
secrets,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
await validateServiceTokenDataClientForSecrets({
|
|
||||||
serviceTokenData: authData.authPayload,
|
|
||||||
secrets,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForSecrets({
|
|
||||||
user: authData.authPayload,
|
|
||||||
secrets,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for secrets resource'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize secret blind index data by setting previously
|
|
||||||
* un-initialized projects to have secret blind index data
|
|
||||||
* (Ensures that all projects have associated blind index data)
|
|
||||||
*/
|
|
||||||
const initSecretBlindIndexDataHelper = async () => {
|
|
||||||
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
|
|
||||||
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
|
|
||||||
_id: {
|
|
||||||
$nin: workspaceIdsBlindIndexed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretBlindIndexDataToInsert = await Promise.all(
|
|
||||||
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
|
|
||||||
const salt = crypto.randomBytes(16).toString('base64');
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedSaltCiphertext,
|
|
||||||
iv: saltIV,
|
|
||||||
tag: saltTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: salt,
|
|
||||||
key: await getEncryptionKey()
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretBlindIndexData = new SecretBlindIndexData({
|
|
||||||
workspace: workspaceToBlindIndex,
|
|
||||||
encryptedSaltCiphertext,
|
|
||||||
saltIV,
|
|
||||||
saltTag
|
|
||||||
})
|
|
||||||
|
|
||||||
return secretBlindIndexData;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (secretBlindIndexDataToInsert.length > 0) {
|
|
||||||
await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret blind index data containing encrypted blind index [salt]
|
* Create secret blind index data containing encrypted blind index [salt]
|
||||||
* for workspace with id [workspaceId]
|
* for workspace with id [workspaceId]
|
||||||
@@ -273,26 +57,47 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// initialize random blind index salt for workspace
|
// initialize random blind index salt for workspace
|
||||||
const salt = crypto.randomBytes(16).toString('base64');
|
const salt = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
const {
|
const encryptionKey = await getEncryptionKey();
|
||||||
ciphertext: encryptedSaltCiphertext,
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
iv: saltIV,
|
|
||||||
tag: saltTag
|
if (rootEncryptionKey) {
|
||||||
} = encryptSymmetric({
|
const {
|
||||||
plaintext: salt,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
key: await getEncryptionKey()
|
iv: saltIV,
|
||||||
});
|
tag: saltTag
|
||||||
|
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||||
const secretBlindIndexData = await new SecretBlindIndexData({
|
|
||||||
workspace: workspaceId,
|
return await new SecretBlindIndexData({
|
||||||
encryptedSaltCiphertext,
|
workspace: workspaceId,
|
||||||
saltIV,
|
encryptedSaltCiphertext,
|
||||||
saltTag
|
saltIV,
|
||||||
}).save();
|
saltTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
return secretBlindIndexData;
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
}).save();
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSaltCiphertext,
|
||||||
|
iv: saltIV,
|
||||||
|
tag: saltTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: salt,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return await new SecretBlindIndexData({
|
||||||
|
workspace: workspaceId,
|
||||||
|
encryptedSaltCiphertext,
|
||||||
|
saltIV,
|
||||||
|
saltTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,22 +111,36 @@ const getSecretBlindIndexSaltHelper = async ({
|
|||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
// check if workspace blind index data exists
|
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
}).select('+algorithm +keyEncoding');
|
||||||
|
|
||||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
|
|
||||||
// decrypt workspace salt
|
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||||
const salt = decryptSymmetric({
|
return client.decryptSymmetric(
|
||||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
iv: secretBlindIndexData.saltIV,
|
rootEncryptionKey,
|
||||||
tag: secretBlindIndexData.saltTag,
|
secretBlindIndexData.saltIV,
|
||||||
key: await getEncryptionKey()
|
secretBlindIndexData.saltTag
|
||||||
|
);
|
||||||
|
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||||
|
// decrypt workspace salt
|
||||||
|
return decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
|
iv: secretBlindIndexData.saltIV,
|
||||||
|
tag: secretBlindIndexData.saltTag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: 'Failed to obtain workspace salt needed for secret blind indexing'
|
||||||
});
|
});
|
||||||
|
|
||||||
return salt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -376,7 +195,7 @@ const generateSecretBlindIndexHelper = async ({
|
|||||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
|
|
||||||
// decrypt workspace salt
|
// decrypt workspace salt
|
||||||
const salt = decryptSymmetric({
|
const salt = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
iv: secretBlindIndexData.saltIV,
|
iv: secretBlindIndexData.saltIV,
|
||||||
tag: secretBlindIndexData.saltTag,
|
tag: secretBlindIndexData.saltTag,
|
||||||
@@ -464,7 +283,9 @@ const createSecretHelper = async ({
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag
|
secretCommentTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
const secretVersion = new SecretVersion({
|
const secretVersion = new SecretVersion({
|
||||||
@@ -481,7 +302,9 @@ const createSecretHelper = async ({
|
|||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag
|
secretValueTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
|
|
||||||
// // (EE) add version for new secret
|
// // (EE) add version for new secret
|
||||||
@@ -771,7 +594,9 @@ const updateSecretHelper = async ({
|
|||||||
secretKeyTag: secret.secretKeyTag,
|
secretKeyTag: secret.secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag
|
secretValueTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) add version for new secret
|
// (EE) add version for new secret
|
||||||
@@ -932,9 +757,6 @@ const deleteSecretHelper = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForSecret,
|
|
||||||
validateClientForSecrets,
|
|
||||||
initSecretBlindIndexDataHelper,
|
|
||||||
createSecretBlindIndexDataHelper,
|
createSecretBlindIndexDataHelper,
|
||||||
getSecretBlindIndexSaltHelper,
|
getSecretBlindIndexSaltHelper,
|
||||||
generateSecretBlindIndexWithSaltHelper,
|
generateSecretBlindIndexWithSaltHelper,
|
||||||
|
|||||||
+4
-222
@@ -1,24 +1,8 @@
|
|||||||
import { Types } from 'mongoose';
|
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
ISecret,
|
|
||||||
IServiceAccount,
|
|
||||||
User,
|
User,
|
||||||
Membership,
|
|
||||||
IOrganization,
|
|
||||||
Organization,
|
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { sendMail } from './nodemailer';
|
import { sendMail } from './nodemailer';
|
||||||
import { validateMembership } from './membership';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
|
||||||
import {
|
|
||||||
validateMembershipOrg
|
|
||||||
} from '../helpers/membershipOrg';
|
|
||||||
import {
|
|
||||||
PERMISSION_READ_SECRETS,
|
|
||||||
PERMISSION_WRITE_SECRETS
|
|
||||||
} from '../variables';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a user under email [email]
|
* Initialize a user under email [email]
|
||||||
@@ -26,7 +10,7 @@ import {
|
|||||||
* @param {String} obj.email - email of user to initialize
|
* @param {String} obj.email - email of user to initialize
|
||||||
* @returns {Object} user - the initialized user
|
* @returns {Object} user - the initialized user
|
||||||
*/
|
*/
|
||||||
const setupAccount = async ({ email }: { email: string }) => {
|
export const setupAccount = async ({ email }: { email: string }) => {
|
||||||
const user = await new User({
|
const user = await new User({
|
||||||
email
|
email
|
||||||
}).save();
|
}).save();
|
||||||
@@ -52,7 +36,7 @@ const setupAccount = async ({ email }: { email: string }) => {
|
|||||||
* @param {String} obj.verifier - verifier for auth SRP
|
* @param {String} obj.verifier - verifier for auth SRP
|
||||||
* @returns {Object} user - the completed user
|
* @returns {Object} user - the completed user
|
||||||
*/
|
*/
|
||||||
const completeAccount = async ({
|
export const completeAccount = async ({
|
||||||
userId,
|
userId,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
@@ -113,7 +97,7 @@ const completeAccount = async ({
|
|||||||
* @param {String} obj.ip - login ip address
|
* @param {String} obj.ip - login ip address
|
||||||
* @param {String} obj.userAgent - login user-agent
|
* @param {String} obj.userAgent - login user-agent
|
||||||
*/
|
*/
|
||||||
const checkUserDevice = async ({
|
export const checkUserDevice = async ({
|
||||||
user,
|
user,
|
||||||
ip,
|
ip,
|
||||||
userAgent
|
userAgent
|
||||||
@@ -148,206 +132,4 @@ const checkUserDevice = async ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that user (client) can access workspace
|
|
||||||
* with id [workspaceId] and its environment [environment] with required permissions
|
|
||||||
* [requiredPermissions]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {User} obj.user - user client
|
|
||||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
|
||||||
* @param {String} environment - (optional) environment in workspace to validate against
|
|
||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateUserClientForWorkspace = async ({
|
|
||||||
user,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
}: {
|
|
||||||
user: IUser;
|
|
||||||
workspaceId: Types.ObjectId;
|
|
||||||
environment?: string;
|
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
|
||||||
requiredPermissions?: string[];
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
// validate user membership in workspace
|
|
||||||
const membership = await validateMembership({
|
|
||||||
userId: user._id,
|
|
||||||
workspaceId,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
let runningIsDisallowed = false;
|
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
|
||||||
switch (requiredPermission) {
|
|
||||||
case PERMISSION_READ_SECRETS:
|
|
||||||
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
|
||||||
break;
|
|
||||||
case PERMISSION_WRITE_SECRETS:
|
|
||||||
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runningIsDisallowed) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that user (client) can access secret [secret]
|
|
||||||
* with required permissions [requiredPermissions]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {User} obj.user - user client
|
|
||||||
* @param {Secret[]} obj.secrets - secrets to validate against
|
|
||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateUserClientForSecret = async ({
|
|
||||||
user,
|
|
||||||
secret,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
}: {
|
|
||||||
user: IUser;
|
|
||||||
secret: ISecret;
|
|
||||||
acceptedRoles?: Array<'admin' | 'member'>;
|
|
||||||
requiredPermissions?: string[];
|
|
||||||
}) => {
|
|
||||||
const membership = await validateMembership({
|
|
||||||
userId: user._id,
|
|
||||||
workspaceId: secret.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
|
|
||||||
const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
|
|
||||||
|
|
||||||
if (isDisallowed) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'You do not have the required permissions to perform this action'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that user (client) can access secrets [secrets]
|
|
||||||
* with required permissions [requiredPermissions]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {User} obj.user - user client
|
|
||||||
* @param {Secret[]} obj.secrets - secrets to validate against
|
|
||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateUserClientForSecrets = async ({
|
|
||||||
user,
|
|
||||||
secrets,
|
|
||||||
requiredPermissions
|
|
||||||
}: {
|
|
||||||
user: IUser;
|
|
||||||
secrets: ISecret[];
|
|
||||||
requiredPermissions?: string[];
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
// TODO: add acceptedRoles?
|
|
||||||
|
|
||||||
const userMemberships = await Membership.find({ user: user._id })
|
|
||||||
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
|
||||||
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
|
|
||||||
|
|
||||||
// for each secret check if the secret belongs to a workspace the user is a member of
|
|
||||||
secrets.forEach((secret: ISecret) => {
|
|
||||||
if (!workspaceIdsSet.has(secret.workspace.toString())) {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: 'Failed authorization for the secret'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
|
|
||||||
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
|
|
||||||
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
|
|
||||||
|
|
||||||
if (isDisallowed) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'You do not have the required permissions to perform this action'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that user (client) can access service account [serviceAccount]
|
|
||||||
* with required permissions [requiredPermissions]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {User} obj.user - user client
|
|
||||||
* @param {ServiceAccount} obj.serviceAccount - service account to validate against
|
|
||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateUserClientForServiceAccount = async ({
|
|
||||||
user,
|
|
||||||
serviceAccount,
|
|
||||||
requiredPermissions
|
|
||||||
}: {
|
|
||||||
user: IUser;
|
|
||||||
serviceAccount: IServiceAccount;
|
|
||||||
requiredPermissions?: string[];
|
|
||||||
}) => {
|
|
||||||
if (!serviceAccount.user.equals(user._id)) {
|
|
||||||
// case: user who created service account is not the
|
|
||||||
// same user that is on the request
|
|
||||||
await validateMembershipOrg({
|
|
||||||
userId: user._id,
|
|
||||||
organizationId: serviceAccount.organization,
|
|
||||||
acceptedRoles: [],
|
|
||||||
acceptedStatuses: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that user (client) can access organization [organization]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {User} obj.user - user client
|
|
||||||
* @param {Organization} obj.organization - organization to validate against
|
|
||||||
*/
|
|
||||||
const validateUserClientForOrganization = async ({
|
|
||||||
user,
|
|
||||||
organization,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses
|
|
||||||
}: {
|
|
||||||
user: IUser;
|
|
||||||
organization: IOrganization;
|
|
||||||
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
|
||||||
acceptedStatuses: Array<'invited' | 'accepted'>;
|
|
||||||
}) => {
|
|
||||||
const membershipOrg = await validateMembershipOrg({
|
|
||||||
userId: user._id,
|
|
||||||
organizationId: organization._id,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses
|
|
||||||
});
|
|
||||||
|
|
||||||
return membershipOrg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
setupAccount,
|
|
||||||
completeAccount,
|
|
||||||
checkUserDevice,
|
|
||||||
validateUserClientForWorkspace,
|
|
||||||
validateUserClientForSecrets,
|
|
||||||
validateUserClientForServiceAccount,
|
|
||||||
validateUserClientForOrganization,
|
|
||||||
validateUserClientForSecret
|
|
||||||
};
|
|
||||||
@@ -1,136 +1,14 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { Types } from 'mongoose';
|
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
Bot,
|
Bot,
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key,
|
||||||
Secret,
|
Secret
|
||||||
User,
|
|
||||||
IUser,
|
|
||||||
ServiceAccountWorkspacePermission,
|
|
||||||
ServiceAccount,
|
|
||||||
IServiceAccount,
|
|
||||||
ServiceTokenData,
|
|
||||||
IServiceTokenData,
|
|
||||||
SecretBlindIndexData
|
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { createBot } from '../helpers/bot';
|
import { createBot } from '../helpers/bot';
|
||||||
import { validateUserClientForWorkspace } from '../helpers/user';
|
|
||||||
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
|
||||||
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
|
|
||||||
import { validateMembership } from '../helpers/membership';
|
|
||||||
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
|
|
||||||
import {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY
|
|
||||||
} from '../variables';
|
|
||||||
import { encryptSymmetric } from '../utils/crypto';
|
|
||||||
import { SecretService } from '../services';
|
import { SecretService } from '../services';
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate authenticated clients for workspace with id [workspaceId] based
|
|
||||||
* on any known permissions.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.authData - authenticated client details
|
|
||||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
|
||||||
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
|
||||||
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
|
||||||
*/
|
|
||||||
const validateClientForWorkspace = async ({
|
|
||||||
authData,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions,
|
|
||||||
requireBlindIndicesEnabled
|
|
||||||
}: {
|
|
||||||
authData: {
|
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
workspaceId: Types.ObjectId;
|
|
||||||
environment?: string;
|
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
|
||||||
requiredPermissions?: string[];
|
|
||||||
requireBlindIndicesEnabled: boolean;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
|
||||||
|
|
||||||
if (!workspace) throw WorkspaceNotFoundError({
|
|
||||||
message: 'Failed to find workspace'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (requireBlindIndicesEnabled) {
|
|
||||||
// case: blind indices are not enabled for secrets in this workspace
|
|
||||||
// (i.e. workspace was created before blind indices were introduced
|
|
||||||
// and no admin has enabled it)
|
|
||||||
|
|
||||||
const secretBlindIndexData = await SecretBlindIndexData.exists({
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!secretBlindIndexData) throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed workspace authorization due to blind indices not being enabled'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
const membership = await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return ({ membership, workspace });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
|
||||||
await validateServiceAccountClientForWorkspace({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
await validateServiceTokenDataClientForWorkspace({
|
|
||||||
serviceTokenData: authData.authPayload,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
const membership = await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
acceptedRoles,
|
|
||||||
requiredPermissions
|
|
||||||
});
|
|
||||||
|
|
||||||
return ({ membership, workspace });
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for workspace'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a workspace with name [name] in organization with id [organizationId]
|
* Create a workspace with name [name] in organization with id [organizationId]
|
||||||
* and a bot for it.
|
* and a bot for it.
|
||||||
@@ -203,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForWorkspace,
|
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
deleteWorkspace
|
deleteWorkspace
|
||||||
};
|
};
|
||||||
|
|||||||
+74
-104
@@ -1,34 +1,26 @@
|
|||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
import express from "express";
|
import express from 'express';
|
||||||
import helmet from "helmet";
|
import helmet from 'helmet';
|
||||||
import cors from "cors";
|
import cors from 'cors';
|
||||||
import * as Sentry from "@sentry/node";
|
import { DatabaseService } from './services';
|
||||||
import { DatabaseService } from "./services";
|
import { EELicenseService } from './ee/services';
|
||||||
import { EELicenseService } from "./ee/services";
|
import { setUpHealthEndpoint } from './services/health';
|
||||||
import { setUpHealthEndpoint } from "./services/health";
|
import cookieParser from 'cookie-parser';
|
||||||
import { initSmtp } from "./services/smtp";
|
import swaggerUi = require('swagger-ui-express');
|
||||||
import { TelemetryService } from "./services";
|
|
||||||
import { setTransporter } from "./helpers/nodemailer";
|
|
||||||
import { createTestUserForDevelopment } from "./utils/addDevelopmentUser";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { patchRouterParam } = require("./utils/patchAsyncRoutes");
|
const swaggerFile = require('../spec.json');
|
||||||
|
|
||||||
import cookieParser from "cookie-parser";
|
|
||||||
import swaggerUi = require("swagger-ui-express");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const swaggerFile = require("../spec.json");
|
const requestIp = require('request-ip');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
import { apiLimiter } from './helpers/rateLimiter';
|
||||||
const requestIp = require("request-ip");
|
|
||||||
import { apiLimiter } from "./helpers/rateLimiter";
|
|
||||||
import {
|
import {
|
||||||
workspace as eeWorkspaceRouter,
|
workspace as eeWorkspaceRouter,
|
||||||
secret as eeSecretRouter,
|
secret as eeSecretRouter,
|
||||||
secretSnapshot as eeSecretSnapshotRouter,
|
secretSnapshot as eeSecretSnapshotRouter,
|
||||||
action as eeActionRouter,
|
action as eeActionRouter,
|
||||||
organizations as eeOrganizationsRouter,
|
organizations as eeOrganizationsRouter,
|
||||||
cloudProducts as eeCloudProductsRouter,
|
cloudProducts as eeCloudProductsRouter
|
||||||
} from "./ee/routes/v1";
|
} from './ee/routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v1SignupRouter,
|
signup as v1SignupRouter,
|
||||||
auth as v1AuthRouter,
|
auth as v1AuthRouter,
|
||||||
@@ -47,8 +39,8 @@ import {
|
|||||||
stripe as v1StripeRouter,
|
stripe as v1StripeRouter,
|
||||||
integration as v1IntegrationRouter,
|
integration as v1IntegrationRouter,
|
||||||
integrationAuth as v1IntegrationAuthRouter,
|
integrationAuth as v1IntegrationAuthRouter,
|
||||||
secretsFolder as v1SecretsFolder,
|
secretsFolder as v1SecretsFolder
|
||||||
} from "./routes/v1";
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v2SignupRouter,
|
signup as v2SignupRouter,
|
||||||
auth as v2AuthRouter,
|
auth as v2AuthRouter,
|
||||||
@@ -62,130 +54,109 @@ import {
|
|||||||
apiKeyData as v2APIKeyDataRouter,
|
apiKeyData as v2APIKeyDataRouter,
|
||||||
environment as v2EnvironmentRouter,
|
environment as v2EnvironmentRouter,
|
||||||
tags as v2TagsRouter,
|
tags as v2TagsRouter,
|
||||||
} from "./routes/v2";
|
} from './routes/v2';
|
||||||
import {
|
import {
|
||||||
auth as v3AuthRouter,
|
auth as v3AuthRouter,
|
||||||
secrets as v3SecretsRouter,
|
secrets as v3SecretsRouter,
|
||||||
signup as v3SignupRouter,
|
signup as v3SignupRouter,
|
||||||
workspaces as v3WorkspacesRouter,
|
workspaces as v3WorkspacesRouter
|
||||||
} from "./routes/v3";
|
} from './routes/v3';
|
||||||
import { healthCheck } from "./routes/status";
|
import { healthCheck } from './routes/status';
|
||||||
import { getLogger } from "./utils/logger";
|
import { getLogger } from './utils/logger';
|
||||||
import { RouteNotFoundError } from "./utils/errors";
|
import { RouteNotFoundError } from './utils/errors';
|
||||||
import { requestErrorHandler } from "./middleware/requestErrorHandler";
|
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||||
import {
|
import {
|
||||||
getMongoURL,
|
|
||||||
getNodeEnv,
|
getNodeEnv,
|
||||||
getPort,
|
getPort,
|
||||||
getSentryDSN,
|
getSiteURL
|
||||||
getSiteURL,
|
} from './config';
|
||||||
} from "./config";
|
import { setup } from './utils/setup';
|
||||||
import { initializePassport } from "./utils/auth";
|
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
TelemetryService.logTelemetryMessage();
|
await setup();
|
||||||
setTransporter(await initSmtp());
|
|
||||||
|
|
||||||
await EELicenseService.initGlobalFeatureSet();
|
await EELicenseService.initGlobalFeatureSet();
|
||||||
|
|
||||||
await DatabaseService.initDatabase(await getMongoURL());
|
|
||||||
if ((await getNodeEnv()) !== "test") {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: await getSentryDSN(),
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
debug: (await getNodeEnv()) === "production" ? false : true,
|
|
||||||
environment: await getNodeEnv(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
patchRouterParam();
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
app.enable('trust proxy');
|
||||||
await initializePassport();
|
|
||||||
|
|
||||||
app.enable("trust proxy");
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
credentials: true,
|
credentials: true,
|
||||||
origin: await getSiteURL(),
|
origin: await getSiteURL()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(requestIp.mw());
|
app.use(requestIp.mw());
|
||||||
|
|
||||||
if ((await getNodeEnv()) === "production") {
|
if ((await getNodeEnv()) === 'production') {
|
||||||
// enable app-wide rate-limiting + helmet security
|
// enable app-wide rate-limiting + helmet security
|
||||||
// in production
|
// in production
|
||||||
app.disable("x-powered-by");
|
app.disable('x-powered-by');
|
||||||
app.use(apiLimiter);
|
app.use(apiLimiter);
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
}
|
}
|
||||||
|
|
||||||
// (EE) routes
|
// (EE) routes
|
||||||
app.use("/api/v1/secret", eeSecretRouter);
|
app.use('/api/v1/secret', eeSecretRouter);
|
||||||
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
|
||||||
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||||
app.use("/api/v1/action", eeActionRouter);
|
app.use('/api/v1/action', eeActionRouter);
|
||||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
app.use('/api/v1/organizations', eeOrganizationsRouter);
|
||||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
|
||||||
|
|
||||||
// v1 routes (default)
|
// v1 routes (default)
|
||||||
app.use("/api/v1/signup", v1SignupRouter);
|
app.use('/api/v1/signup', v1SignupRouter);
|
||||||
app.use("/api/v1/auth", v1AuthRouter);
|
app.use('/api/v1/auth', v1AuthRouter);
|
||||||
app.use("/api/v1/bot", v1BotRouter);
|
app.use('/api/v1/bot', v1BotRouter);
|
||||||
app.use("/api/v1/user", v1UserRouter);
|
app.use('/api/v1/user', v1UserRouter);
|
||||||
app.use("/api/v1/user-action", v1UserActionRouter);
|
app.use('/api/v1/user-action', v1UserActionRouter);
|
||||||
app.use("/api/v1/organization", v1OrganizationRouter);
|
app.use('/api/v1/organization', v1OrganizationRouter);
|
||||||
app.use("/api/v1/workspace", v1WorkspaceRouter);
|
app.use('/api/v1/workspace', v1WorkspaceRouter);
|
||||||
app.use("/api/v1/membership-org", v1MembershipOrgRouter);
|
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
|
||||||
app.use("/api/v1/membership", v1MembershipRouter);
|
app.use('/api/v1/membership', v1MembershipRouter);
|
||||||
app.use("/api/v1/key", v1KeyRouter);
|
app.use('/api/v1/key', v1KeyRouter);
|
||||||
app.use("/api/v1/invite-org", v1InviteOrgRouter);
|
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||||
app.use("/api/v1/secret", v1SecretRouter);
|
app.use('/api/v1/secret', v1SecretRouter); // deprecate
|
||||||
app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecated
|
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
|
||||||
app.use("/api/v1/password", v1PasswordRouter);
|
app.use('/api/v1/password', v1PasswordRouter);
|
||||||
app.use("/api/v1/stripe", v1StripeRouter);
|
app.use('/api/v1/stripe', v1StripeRouter);
|
||||||
app.use("/api/v1/integration", v1IntegrationRouter);
|
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||||
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
app.use("/api/v1/folder", v1SecretsFolder);
|
app.use('/api/v1/folder', v1SecretsFolder)
|
||||||
|
|
||||||
// v2 routes (improvements)
|
// v2 routes (improvements)
|
||||||
app.use("/api/v2/signup", v2SignupRouter);
|
app.use('/api/v2/signup', v2SignupRouter);
|
||||||
app.use("/api/v2/auth", v2AuthRouter);
|
app.use('/api/v2/auth', v2AuthRouter);
|
||||||
app.use("/api/v2/users", v2UsersRouter);
|
app.use('/api/v2/users', v2UsersRouter);
|
||||||
app.use("/api/v2/organizations", v2OrganizationsRouter);
|
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
||||||
app.use("/api/v2/workspace", v2EnvironmentRouter);
|
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||||
app.use("/api/v2/workspace", v2TagsRouter);
|
app.use('/api/v2/workspace', v2TagsRouter);
|
||||||
app.use("/api/v2/workspace", v2WorkspaceRouter);
|
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||||
app.use("/api/v2/secret", v2SecretRouter); // deprecated
|
app.use('/api/v2/secret', v2SecretRouter); // deprecate
|
||||||
app.use("/api/v2/secrets", v2SecretsRouter);
|
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter); // TODO: turn into plural route
|
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
|
||||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
|
||||||
app.use("/api/v2/api-key", v2APIKeyDataRouter);
|
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
||||||
|
|
||||||
// v3 routes (experimental)
|
// v3 routes (experimental)
|
||||||
app.use("/api/v3/auth", v3AuthRouter);
|
app.use("/api/v3/auth", v3AuthRouter);
|
||||||
app.use("/api/v3/secrets", v3SecretsRouter);
|
app.use('/api/v3/secrets', v3SecretsRouter);
|
||||||
app.use("/api/v3/workspaces", v3WorkspacesRouter);
|
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
||||||
app.use("/api/v3/signup", v3SignupRouter);
|
app.use("/api/v3/signup", v3SignupRouter);
|
||||||
|
|
||||||
// api docs
|
// api docs
|
||||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||||
|
|
||||||
// Server status
|
// server status
|
||||||
app.use("/api", healthCheck);
|
app.use('/api', healthCheck)
|
||||||
|
|
||||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (res.headersSent) return next();
|
if (res.headersSent) return next();
|
||||||
next(
|
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
|
||||||
RouteNotFoundError({
|
})
|
||||||
message: `The requested source '(${req.method})${req.url}' was not found`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(requestErrorHandler);
|
app.use(requestErrorHandler);
|
||||||
|
|
||||||
@@ -195,7 +166,6 @@ const main = async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await createTestUserForDevelopment();
|
|
||||||
setUpHealthEndpoint(server);
|
setUpHealthEndpoint(server);
|
||||||
|
|
||||||
server.on("close", async () => {
|
server.on("close", async () => {
|
||||||
|
|||||||
@@ -1559,20 +1559,35 @@ const syncSecretsGitLab = async ({
|
|||||||
environment_scope: string;
|
environment_scope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get secrets from gitlab
|
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
|
||||||
const getSecretsRes: GitLabSecret[] = (
|
const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
|
||||||
await standardRequest.get(
|
const headers = {
|
||||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
|
"Authorization": `Bearer ${accessToken}`,
|
||||||
{
|
"Accept-Encoding": "application/json",
|
||||||
headers: {
|
};
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json",
|
let allEnvVariables: GitLabSecret[] = [];
|
||||||
},
|
let url: string | null = `${gitLabApiUrl}?per_page=100`;
|
||||||
|
|
||||||
|
while (url) {
|
||||||
|
const response: any = await standardRequest.get(url, { headers });
|
||||||
|
allEnvVariables = [...allEnvVariables, ...response.data];
|
||||||
|
|
||||||
|
const linkHeader = response.headers.link;
|
||||||
|
const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"'));
|
||||||
|
|
||||||
|
if (nextLink) {
|
||||||
|
url = nextLink.trim().split(';')[0].slice(1, -1);
|
||||||
|
} else {
|
||||||
|
url = null;
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
)
|
|
||||||
.data
|
return allEnvVariables;
|
||||||
.filter((secret: GitLabSecret) =>
|
};
|
||||||
|
|
||||||
|
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
|
||||||
|
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
|
||||||
secret.environment_scope === integration.targetEnvironment
|
secret.environment_scope === integration.targetEnvironment
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1631,6 +1646,7 @@ const syncSecretsGitLab = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export interface IGenerateKeyPairOutput {
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEncryptAsymmetricInput {
|
||||||
|
plaintext: string;
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEncryptAsymmetricOutput {
|
||||||
|
ciphertext: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDecryptAsymmetricInput {
|
||||||
|
ciphertext: string;
|
||||||
|
nonce: string;
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEncryptSymmetricInput {
|
||||||
|
plaintext: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEncryptSymmetricOutput {
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDecryptSymmetricInput {
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './crypto';
|
||||||
@@ -7,9 +7,6 @@ import {
|
|||||||
getAuthAPIKeyPayload,
|
getAuthAPIKeyPayload,
|
||||||
getAuthSAAKPayload
|
getAuthSAAKPayload
|
||||||
} from '../helpers/auth';
|
} from '../helpers/auth';
|
||||||
import {
|
|
||||||
UnauthorizedRequestError
|
|
||||||
} from '../utils/errors';
|
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
@@ -45,57 +42,61 @@ const requireAuth = ({
|
|||||||
acceptedAuthModes: string[];
|
acceptedAuthModes: string[];
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|
||||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||||
// and return token type [authTokenType] and value [authTokenValue]
|
// and return token type [authTokenType] and value [authTokenValue]
|
||||||
const { authMode, authTokenValue } = validateAuthMode({
|
try {
|
||||||
headers: req.headers,
|
const { authMode, authTokenValue } = validateAuthMode({
|
||||||
acceptedAuthModes
|
headers: req.headers,
|
||||||
});
|
acceptedAuthModes
|
||||||
|
});
|
||||||
|
|
||||||
let authPayload: IUser | IServiceAccount | IServiceTokenData;
|
let authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
case AUTH_MODE_SERVICE_ACCOUNT:
|
case AUTH_MODE_SERVICE_ACCOUNT:
|
||||||
authPayload = await getAuthSAAKPayload({
|
authPayload = await getAuthSAAKPayload({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
});
|
});
|
||||||
req.serviceAccount = authPayload;
|
req.serviceAccount = authPayload;
|
||||||
break;
|
break;
|
||||||
case AUTH_MODE_SERVICE_TOKEN:
|
case AUTH_MODE_SERVICE_TOKEN:
|
||||||
authPayload = await getAuthSTDPayload({
|
authPayload = await getAuthSTDPayload({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
});
|
});
|
||||||
req.serviceTokenData = authPayload;
|
req.serviceTokenData = authPayload;
|
||||||
break;
|
break;
|
||||||
case AUTH_MODE_API_KEY:
|
case AUTH_MODE_API_KEY:
|
||||||
authPayload = await getAuthAPIKeyPayload({
|
authPayload = await getAuthAPIKeyPayload({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
});
|
});
|
||||||
req.user = authPayload;
|
req.user = authPayload;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
authPayload = await getAuthUserPayload({
|
authPayload = await getAuthUserPayload({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
});
|
});
|
||||||
req.user = authPayload;
|
req.user = authPayload;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.requestData = {
|
||||||
|
...req.params,
|
||||||
|
...req.query,
|
||||||
|
...req.body,
|
||||||
|
}
|
||||||
|
|
||||||
|
req.authData = {
|
||||||
|
authMode,
|
||||||
|
authPayload, // User, ServiceAccount, ServiceTokenData
|
||||||
|
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
|
authIP: req.ip,
|
||||||
|
authUserAgent: req.headers['user-agent'] ?? 'other'
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.requestData = {
|
|
||||||
...req.params,
|
|
||||||
...req.query,
|
|
||||||
...req.body,
|
|
||||||
}
|
|
||||||
|
|
||||||
req.authData = {
|
|
||||||
authMode,
|
|
||||||
authPayload, // User, ServiceAccount, ServiceTokenData
|
|
||||||
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
|
|
||||||
authIP: req.ip,
|
|
||||||
authUserAgent: req.headers['user-agent'] ?? 'other'
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Bot } from '../models';
|
import { validateClientForBot } from '../validation';
|
||||||
import { validateMembership } from '../helpers/membership';
|
|
||||||
import { validateClientForBot } from '../helpers/bot';
|
|
||||||
import { AccountNotFoundError } from '../utils/errors';
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Integration, IntegrationAuth } from '../models';
|
import { validateClientForIntegration } from '../validation';
|
||||||
import { IntegrationService } from '../services';
|
|
||||||
import { validateMembership } from '../helpers/membership';
|
|
||||||
import { validateClientForIntegration } from '../helpers/integration';
|
|
||||||
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IntegrationAuth, IWorkspace } from '../models';
|
import { validateClientForIntegrationAuth } from '../validation';
|
||||||
import { IntegrationService } from '../services';
|
|
||||||
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
|
|
||||||
import { validateMembership } from '../helpers/membership';
|
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { validateClientForMembership } from '../validation';
|
||||||
import {
|
|
||||||
Membership,
|
|
||||||
} from '../models';
|
|
||||||
import {
|
|
||||||
validateClientForMembership,
|
|
||||||
validateMembership
|
|
||||||
} from '../helpers/membership';
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { validateClientForMembershipOrg } from '../validation';
|
||||||
import {
|
|
||||||
MembershipOrg
|
|
||||||
} from '../models';
|
|
||||||
import {
|
|
||||||
validateClientForMembershipOrg,
|
|
||||||
validateMembershipOrg
|
|
||||||
} from '../helpers/membershipOrg';
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: transform
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { IOrganization, MembershipOrg } from '../models';
|
import { validateClientForOrganization } from '../validation';
|
||||||
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
|
|
||||||
import { validateMembershipOrg } from '../helpers/membershipOrg';
|
|
||||||
import { validateClientForOrganization } from '../helpers/organization';
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
|
import { validateClientForSecret } from '../validation';
|
||||||
import { Secret } from '../models';
|
|
||||||
import {
|
|
||||||
validateMembership
|
|
||||||
} from '../helpers/membership';
|
|
||||||
import {
|
|
||||||
validateClientForSecret
|
|
||||||
} from '../helpers/secrets';
|
|
||||||
|
|
||||||
// note: used for old /v1/secret and /v2/secret routes.
|
// note: used for old /v1/secret and /v2/secret routes.
|
||||||
// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception
|
// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { validateClientForSecrets } from '../validation';
|
||||||
import { Secret, Membership } from '../models';
|
|
||||||
import { validateClientForSecrets } from '../helpers/secrets';
|
|
||||||
|
|
||||||
const requireSecretsAuth = ({
|
const requireSecretsAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { ServiceAccount } from '../models';
|
import { validateClientForServiceAccount } from '../validation';
|
||||||
import {
|
|
||||||
ServiceAccountNotFoundError
|
|
||||||
} from '../utils/errors';
|
|
||||||
import {
|
|
||||||
validateMembershipOrg
|
|
||||||
} from '../helpers/membershipOrg';
|
|
||||||
import {
|
|
||||||
validateClientForServiceAccount
|
|
||||||
} from '../helpers/serviceAccount';
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { ServiceToken, ServiceTokenData } from '../models';
|
import { validateClientForServiceTokenData } from '../validation';
|
||||||
import { validateClientForServiceTokenData } from '../helpers/serviceTokenData';
|
|
||||||
import { validateMembership } from '../helpers/membership';
|
|
||||||
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
|
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { validateClientForWorkspace } from '../helpers/workspace';
|
import { validateClientForWorkspace } from '../validation';
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
export interface IBackupPrivateKey {
|
export interface IBackupPrivateKey {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -7,6 +12,8 @@ export interface IBackupPrivateKey {
|
|||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
|
algorithm: string;
|
||||||
|
keyEncoding: 'base64' | 'utf8';
|
||||||
verifier: string;
|
verifier: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +39,19 @@ const backupPrivateKeySchema = new Schema<IBackupPrivateKey>(
|
|||||||
select: false,
|
select: false,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
algorithm: { // the encryption algorithm used
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
salt: {
|
salt: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false,
|
select: false,
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_HEX,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
export interface IBot {
|
export interface IBot {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -9,6 +15,8 @@ export interface IBot {
|
|||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
|
algorithm: 'aes-256-gcm';
|
||||||
|
keyEncoding: 'base64' | 'utf8';
|
||||||
}
|
}
|
||||||
|
|
||||||
const botSchema = new Schema<IBot>(
|
const botSchema = new Schema<IBot>(
|
||||||
@@ -45,6 +53,21 @@ const botSchema = new Schema<IBot>(
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
select: false
|
select: false
|
||||||
|
},
|
||||||
|
algorithm: { // the encryption algorithm used
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import {
|
|||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
|
||||||
export interface IIntegrationAuth extends Document {
|
export interface IIntegrationAuth extends Document {
|
||||||
@@ -31,6 +34,8 @@ export interface IIntegrationAuth extends Document {
|
|||||||
accessCiphertext?: string;
|
accessCiphertext?: string;
|
||||||
accessIV?: string;
|
accessIV?: string;
|
||||||
accessTag?: string;
|
accessTag?: string;
|
||||||
|
algorithm?: 'aes-256-gcm';
|
||||||
|
keyEncoding?: 'utf8' | 'base64';
|
||||||
accessExpiresAt?: Date;
|
accessExpiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +114,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
type: Date,
|
type: Date,
|
||||||
select: false,
|
select: false,
|
||||||
},
|
},
|
||||||
|
algorithm: { // the encryption algorithm used
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import { ROOT_FOLDER_PATH } from '../utils/folder';
|
import { ROOT_FOLDER_PATH } from '../utils/folder';
|
||||||
|
|
||||||
@@ -25,6 +28,8 @@ export interface ISecret {
|
|||||||
secretCommentIV?: string;
|
secretCommentIV?: string;
|
||||||
secretCommentTag?: string;
|
secretCommentTag?: string;
|
||||||
secretCommentHash?: string;
|
secretCommentHash?: string;
|
||||||
|
algorithm: 'aes-256-gcm';
|
||||||
|
keyEncoding: 'utf8' | 'base64';
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
path?: string;
|
path?: string;
|
||||||
folder?: Types.ObjectId;
|
folder?: Types.ObjectId;
|
||||||
@@ -111,6 +116,19 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
algorithm: { // the encryption algorithm used
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
// the full path to the secret in relation to folders
|
// the full path to the secret in relation to folders
|
||||||
path: {
|
path: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Schema, model, Types, Document } from 'mongoose';
|
import { Schema, model, Types, Document } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
export interface ISecretBlindIndexData extends Document {
|
export interface ISecretBlindIndexData extends Document {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -6,6 +11,8 @@ export interface ISecretBlindIndexData extends Document {
|
|||||||
encryptedSaltCiphertext: string;
|
encryptedSaltCiphertext: string;
|
||||||
saltIV: string;
|
saltIV: string;
|
||||||
saltTag: string;
|
saltTag: string;
|
||||||
|
algorithm: 'aes-256-gcm';
|
||||||
|
keyEncoding: 'base64' | 'utf8'
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
||||||
@@ -15,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
|||||||
ref: 'Workspace',
|
ref: 'Workspace',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
encryptedSaltCiphertext: {
|
encryptedSaltCiphertext: { // TODO: make these select: false
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
@@ -26,7 +33,23 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
|
|||||||
saltTag: {
|
saltTag: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
algorithm: {
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
requireSecretsAuth,
|
requireSecretsAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../middleware';
|
} from '../../middleware';
|
||||||
|
import { validateClientForSecrets } from '../../validation';
|
||||||
import { query, body } from 'express-validator';
|
import { query, body } from 'express-validator';
|
||||||
import { secretsController } from '../../controllers/v2';
|
import { secretsController } from '../../controllers/v2';
|
||||||
import { validateClientForSecrets } from '../../helpers/secrets';
|
|
||||||
import {
|
import {
|
||||||
ADMIN,
|
ADMIN,
|
||||||
MEMBER,
|
MEMBER,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// WIP
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
ISecret
|
ISecret
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
DeleteSecretParams
|
DeleteSecretParams
|
||||||
} from '../interfaces/services/SecretService';
|
} from '../interfaces/services/SecretService';
|
||||||
import {
|
import {
|
||||||
initSecretBlindIndexDataHelper,
|
|
||||||
createSecretBlindIndexDataHelper,
|
createSecretBlindIndexDataHelper,
|
||||||
getSecretBlindIndexSaltHelper,
|
getSecretBlindIndexSaltHelper,
|
||||||
generateSecretBlindIndexWithSaltHelper,
|
generateSecretBlindIndexWithSaltHelper,
|
||||||
@@ -24,16 +22,6 @@ import {
|
|||||||
} from '../helpers/secrets';
|
} from '../helpers/secrets';
|
||||||
|
|
||||||
class SecretService {
|
class SecretService {
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param param0 h
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
|
|
||||||
static async initSecretBlindIndexDataHelper() {
|
|
||||||
return await initSecretBlindIndexDataHelper();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret blind index data containing encrypted blind index salt
|
* Create secret blind index data containing encrypted blind index salt
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
************************************************************************************************/
|
************************************************************************************************/
|
||||||
|
|
||||||
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
|
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
|
||||||
|
import { SecretService } from "../services";
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { getNodeEnv } from '../config';
|
import { getNodeEnv } from '../config';
|
||||||
|
|
||||||
@@ -119,7 +120,12 @@ export const createTestUserForDevelopment = async () => {
|
|||||||
// create workspace if not exist
|
// create workspace if not exist
|
||||||
const workspaceInDB = await Workspace.findById(testWorkspaceId)
|
const workspaceInDB = await Workspace.findById(testWorkspaceId)
|
||||||
if (!workspaceInDB) {
|
if (!workspaceInDB) {
|
||||||
await Workspace.create(testWorkspace)
|
const workspace = await Workspace.create(testWorkspace)
|
||||||
|
|
||||||
|
// initialize blind index salt for workspace
|
||||||
|
await SecretService.createSecretBlindIndexData({
|
||||||
|
workspaceId: workspace._id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// create workspace key if not exist
|
// create workspace key if not exist
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import nacl from 'tweetnacl';
|
|
||||||
import util from 'tweetnacl-util';
|
|
||||||
import AesGCM from './aes-gcm';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return new base64, NaCl, public-private key pair.
|
|
||||||
* @returns {Object} obj
|
|
||||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
|
||||||
* @returns {String} obj.privateKey - base64, NaCl, private key
|
|
||||||
*/
|
|
||||||
const generateKeyPair = () => {
|
|
||||||
const pair = nacl.box.keyPair();
|
|
||||||
|
|
||||||
return ({
|
|
||||||
publicKey: util.encodeBase64(pair.publicKey),
|
|
||||||
privateKey: util.encodeBase64(pair.secretKey)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
|
||||||
* [publicKey] likely belongs to the recipient.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.plaintext - plaintext to encrypt
|
|
||||||
* @param {String} obj.publicKey - public key of the recipient
|
|
||||||
* @param {String} obj.privateKey - private key of the sender (current user)
|
|
||||||
* @returns {Object} obj
|
|
||||||
* @returns {String} ciphertext - base64-encoded ciphertext
|
|
||||||
* @returns {String} nonce - base64-encoded nonce
|
|
||||||
*/
|
|
||||||
const encryptAsymmetric = ({
|
|
||||||
plaintext,
|
|
||||||
publicKey,
|
|
||||||
privateKey
|
|
||||||
}: {
|
|
||||||
plaintext: string;
|
|
||||||
publicKey: string;
|
|
||||||
privateKey: string;
|
|
||||||
}) => {
|
|
||||||
const nonce = nacl.randomBytes(24);
|
|
||||||
const ciphertext = nacl.box(
|
|
||||||
util.decodeUTF8(plaintext),
|
|
||||||
nonce,
|
|
||||||
util.decodeBase64(publicKey),
|
|
||||||
util.decodeBase64(privateKey)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ciphertext: util.encodeBase64(ciphertext),
|
|
||||||
nonce: util.encodeBase64(nonce)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
|
|
||||||
* [privateKey] likely belongs to the recipient.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
|
||||||
* @param {String} obj.nonce - nonce
|
|
||||||
* @param {String} obj.publicKey - public key of the sender
|
|
||||||
* @param {String} obj.privateKey - private key of the receiver (current user)
|
|
||||||
* @param {String} plaintext - UTF8 plaintext
|
|
||||||
*/
|
|
||||||
const decryptAsymmetric = ({
|
|
||||||
ciphertext,
|
|
||||||
nonce,
|
|
||||||
publicKey,
|
|
||||||
privateKey
|
|
||||||
}: {
|
|
||||||
ciphertext: string;
|
|
||||||
nonce: string;
|
|
||||||
publicKey: string;
|
|
||||||
privateKey: string;
|
|
||||||
}): string => {
|
|
||||||
const plaintext: any = nacl.box.open(
|
|
||||||
util.decodeBase64(ciphertext),
|
|
||||||
util.decodeBase64(nonce),
|
|
||||||
util.decodeBase64(publicKey),
|
|
||||||
util.decodeBase64(privateKey)
|
|
||||||
);
|
|
||||||
|
|
||||||
return util.encodeUTF8(plaintext);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return symmetrically encrypted [plaintext] using [key].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.plaintext - plaintext to encrypt
|
|
||||||
* @param {String} obj.key - hex key
|
|
||||||
*/
|
|
||||||
const encryptSymmetric = ({
|
|
||||||
plaintext,
|
|
||||||
key
|
|
||||||
}: {
|
|
||||||
plaintext: string;
|
|
||||||
key: string;
|
|
||||||
}) => {
|
|
||||||
const obj = AesGCM.encrypt(plaintext, key);
|
|
||||||
const { ciphertext, iv, tag } = obj;
|
|
||||||
|
|
||||||
return {
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return symmetrically decypted [ciphertext] using [iv], [tag],
|
|
||||||
* and [key].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
|
||||||
* @param {String} obj.iv - iv
|
|
||||||
* @param {String} obj.tag - tag
|
|
||||||
* @param {String} obj.key - hex key
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const decryptSymmetric = ({
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key
|
|
||||||
}: {
|
|
||||||
ciphertext: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
key: string;
|
|
||||||
}): string => {
|
|
||||||
const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
|
|
||||||
return plaintext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
generateKeyPair,
|
|
||||||
encryptAsymmetric,
|
|
||||||
decryptAsymmetric,
|
|
||||||
encryptSymmetric,
|
|
||||||
decryptSymmetric
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
import nacl from 'tweetnacl';
|
||||||
|
import util from 'tweetnacl-util';
|
||||||
|
import {
|
||||||
|
IGenerateKeyPairOutput,
|
||||||
|
IEncryptAsymmetricInput,
|
||||||
|
IEncryptAsymmetricOutput,
|
||||||
|
IDecryptAsymmetricInput,
|
||||||
|
IEncryptSymmetricInput,
|
||||||
|
IDecryptSymmetricInput
|
||||||
|
} from '../../interfaces/utils';
|
||||||
|
import { BadRequestError } from '../errors';
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
NONCE_BYTES_SIZE,
|
||||||
|
BLOCK_SIZE_BYTES_16
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new base64, NaCl, public-private key pair.
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.publicKey - (base64) NaCl, public key
|
||||||
|
* @returns {String} obj.privateKey - (base64), NaCl, private key
|
||||||
|
*/
|
||||||
|
const generateKeyPair = (): IGenerateKeyPairOutput => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
return ({
|
||||||
|
publicKey: util.encodeBase64(pair.publicKey),
|
||||||
|
privateKey: util.encodeBase64(pair.secretKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||||
|
* [publicKey] likely belongs to the recipient.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
|
* @param {String} obj.publicKey - (base64) Nacl public key of the recipient
|
||||||
|
* @param {String} obj.privateKey - (base64) Nacl private key of the sender (current user)
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.ciphertext - (base64) ciphertext
|
||||||
|
* @returns {String} obj.nonce - (base64) nonce
|
||||||
|
*/
|
||||||
|
const encryptAsymmetric = ({
|
||||||
|
plaintext,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
}: IEncryptAsymmetricInput): IEncryptAsymmetricOutput => {
|
||||||
|
const nonce = nacl.randomBytes(24);
|
||||||
|
const ciphertext = nacl.box(
|
||||||
|
util.decodeUTF8(plaintext),
|
||||||
|
nonce,
|
||||||
|
util.decodeBase64(publicKey),
|
||||||
|
util.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ciphertext: util.encodeBase64(ciphertext),
|
||||||
|
nonce: util.encodeBase64(nonce)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
|
||||||
|
* [privateKey] likely belongs to the recipient.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.nonce - (base64) nonce
|
||||||
|
* @param {String} obj.publicKey - (base64) public key of the sender
|
||||||
|
* @param {String} obj.privateKey - (base64) private key of the receiver (current user)
|
||||||
|
* @returns {String} plaintext - (utf8) plaintext
|
||||||
|
*/
|
||||||
|
const decryptAsymmetric = ({
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
}: IDecryptAsymmetricInput): string => {
|
||||||
|
const plaintext: Uint8Array | null = nacl.box.open(
|
||||||
|
util.decodeBase64(ciphertext),
|
||||||
|
util.decodeBase64(nonce),
|
||||||
|
util.decodeBase64(publicKey),
|
||||||
|
util.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (plaintext == null) throw BadRequestError({
|
||||||
|
message: 'Invalid ciphertext or keys'
|
||||||
|
});
|
||||||
|
|
||||||
|
return util.encodeUTF8(plaintext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using [key].
|
||||||
|
*
|
||||||
|
* NOTE: THIS FUNCTION SHOULD NOT BE USED FOR ALL FUTURE
|
||||||
|
* ENCRYPTION OPERATIONS UNLESS IT TOUCHES OLD FUNCTIONALITY
|
||||||
|
* THAT USES IT. USE encryptSymmetric() instead
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.plaintext - (utf8) plaintext to encrypt
|
||||||
|
* @param {String} obj.key - (hex) 128-bit key
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.ciphertext (base64) ciphertext
|
||||||
|
* @returns {String} obj.iv (base64) iv
|
||||||
|
* @returns {String} obj.tag (base64) tag
|
||||||
|
*/
|
||||||
|
const encryptSymmetric128BitHexKeyUTF8 = ({
|
||||||
|
plaintext,
|
||||||
|
key
|
||||||
|
}: IEncryptSymmetricInput) => {
|
||||||
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, key, iv);
|
||||||
|
|
||||||
|
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
|
||||||
|
ciphertext += cipher.final('base64');
|
||||||
|
|
||||||
|
return {
|
||||||
|
ciphertext,
|
||||||
|
iv: iv.toString('base64'),
|
||||||
|
tag: cipher.getAuthTag().toString('base64')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return symmetrically decrypted [ciphertext] using [iv], [tag],
|
||||||
|
* and [key].
|
||||||
|
*
|
||||||
|
* NOTE: THIS FUNCTION SHOULD NOT BE USED FOR ALL FUTURE
|
||||||
|
* DECRYPTION OPERATIONS UNLESS IT TOUCHES OLD FUNCTIONALITY
|
||||||
|
* THAT USES IT. USE decryptSymmetric() instead
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.iv - (base64) 256-bit iv
|
||||||
|
* @param {String} obj.tag - (base64) tag
|
||||||
|
* @param {String} obj.key - (hex) 128-bit key
|
||||||
|
* @returns {String} cleartext - the deciphered ciphertext
|
||||||
|
*/
|
||||||
|
const decryptSymmetric128BitHexKeyUTF8 = ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
}: IDecryptSymmetricInput) => {
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
key,
|
||||||
|
Buffer.from(iv, 'base64')
|
||||||
|
);
|
||||||
|
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||||
|
|
||||||
|
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
cleartext += decipher.final('utf8');
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
generateKeyPair,
|
||||||
|
encryptAsymmetric,
|
||||||
|
decryptAsymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
decryptSymmetric128BitHexKeyUTF8
|
||||||
|
};
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
import { encryptSymmetric128BitHexKeyUTF8 } from '../crypto';
|
||||||
|
import { EESecretService } from '../../ee/services';
|
||||||
|
import { SecretVersion } from '../../ee/models';
|
||||||
|
import {
|
||||||
|
Secret,
|
||||||
|
ISecret,
|
||||||
|
SecretBlindIndexData,
|
||||||
|
Workspace,
|
||||||
|
Bot,
|
||||||
|
BackupPrivateKey,
|
||||||
|
IntegrationAuth,
|
||||||
|
} from '../../models';
|
||||||
|
import {
|
||||||
|
generateKeyPair
|
||||||
|
} from '../../utils/crypto';
|
||||||
|
import {
|
||||||
|
client,
|
||||||
|
getEncryptionKey,
|
||||||
|
getRootEncryptionKey
|
||||||
|
} from '../../config';
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
} from '../../variables';
|
||||||
|
import { InternalServerError } from '../errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill secrets to ensure that they're all versioned and have
|
||||||
|
* corresponding secret versions
|
||||||
|
*/
|
||||||
|
export const backfillSecretVersions = async () => {
|
||||||
|
await Secret.updateMany(
|
||||||
|
{ version: { $exists: false } },
|
||||||
|
{ $set: { version: 1 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "secretversions",
|
||||||
|
localField: "_id",
|
||||||
|
foreignField: "secret",
|
||||||
|
as: "versions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
versions: { $size: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (unversionedSecrets.length > 0) {
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: unversionedSecrets.map(
|
||||||
|
(s, idx) =>
|
||||||
|
new SecretVersion({
|
||||||
|
...s,
|
||||||
|
secret: s._id,
|
||||||
|
version: s.version ? s.version : 1,
|
||||||
|
isDeleted: false,
|
||||||
|
workspace: s.workspace,
|
||||||
|
environment: s.environment,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill workspace bots to ensure that every workspace has a bot
|
||||||
|
*/
|
||||||
|
export const backfillBots = async () => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
const workspaceIdsWithBot = await Bot.distinct('workspace');
|
||||||
|
const workspaceIdsToAddBot = await Workspace.distinct('_id', {
|
||||||
|
_id: {
|
||||||
|
$nin: workspaceIdsWithBot
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workspaceIdsToAddBot.length === 0) return;
|
||||||
|
|
||||||
|
const botsToInsert = await Promise.all(
|
||||||
|
workspaceIdsToAddBot.map(async (workspaceToAddBot) => {
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
|
||||||
|
if (rootEncryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
return new Bot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspace: workspaceToAddBot,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
});
|
||||||
|
} else if (encryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Bot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspace: workspaceToAddBot,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: 'Failed to backfill workspace bots due to missing encryption key'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Bot.insertMany(botsToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill secret blind index data to ensure that every workspace
|
||||||
|
* has a secret blind index data
|
||||||
|
*/
|
||||||
|
export const backfillSecretBlindIndexData = async () => {
|
||||||
|
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
|
||||||
|
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
|
||||||
|
_id: {
|
||||||
|
$nin: workspaceIdsBlindIndexed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workspaceIdsToBlindIndex.length === 0) return;
|
||||||
|
|
||||||
|
const secretBlindIndexDataToInsert = await Promise.all(
|
||||||
|
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
|
||||||
|
const salt = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
if (rootEncryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSaltCiphertext,
|
||||||
|
iv: saltIV,
|
||||||
|
tag: saltTag
|
||||||
|
} = client.encryptSymmetric(salt, rootEncryptionKey)
|
||||||
|
|
||||||
|
return new SecretBlindIndexData({
|
||||||
|
workspace: workspaceToBlindIndex,
|
||||||
|
encryptedSaltCiphertext,
|
||||||
|
saltIV,
|
||||||
|
saltTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
});
|
||||||
|
} else if (encryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSaltCiphertext,
|
||||||
|
iv: saltIV,
|
||||||
|
tag: saltTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: salt,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return new SecretBlindIndexData({
|
||||||
|
workspace: workspaceToBlindIndex,
|
||||||
|
encryptedSaltCiphertext,
|
||||||
|
saltIV,
|
||||||
|
saltTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: 'Failed to backfill secret blind index data due to missing encryption key'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill Secret, SecretVersion, SecretBlindIndexData, Bot,
|
||||||
|
* BackupPrivateKey, IntegrationAuth collections to ensure that
|
||||||
|
* they all have encryption metadata documented
|
||||||
|
*/
|
||||||
|
export const backfillEncryptionMetadata = async () => {
|
||||||
|
|
||||||
|
// backfill secret encryption metadata
|
||||||
|
await Secret.updateMany(
|
||||||
|
{
|
||||||
|
algorithm: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// backfill secret version encryption metadata
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{
|
||||||
|
algorithm: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// backfill secret blind index encryption metadata
|
||||||
|
await SecretBlindIndexData.updateMany(
|
||||||
|
{
|
||||||
|
algorithm: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// backfill bot encryption metadata
|
||||||
|
await Bot.updateMany(
|
||||||
|
{
|
||||||
|
algorithm: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// backfill backup private key encryption metadata
|
||||||
|
await BackupPrivateKey.updateMany(
|
||||||
|
{
|
||||||
|
algorithm: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// backfill integration auth encryption metadata
|
||||||
|
await IntegrationAuth.updateMany(
|
||||||
|
{
|
||||||
|
algorithm: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { DatabaseService, TelemetryService } from '../../services';
|
||||||
|
import { setTransporter } from '../../helpers/nodemailer';
|
||||||
|
import { EELicenseService } from '../../ee/services';
|
||||||
|
import { initSmtp } from '../../services/smtp';
|
||||||
|
import { createTestUserForDevelopment } from '../addDevelopmentUser'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { patchRouterParam } = require('../patchAsyncRoutes');
|
||||||
|
import {
|
||||||
|
validateEncryptionKeysConfig
|
||||||
|
} from './validateConfig';
|
||||||
|
import {
|
||||||
|
backfillSecretVersions,
|
||||||
|
backfillBots,
|
||||||
|
backfillSecretBlindIndexData,
|
||||||
|
backfillEncryptionMetadata
|
||||||
|
} from './backfillData';
|
||||||
|
import {
|
||||||
|
reencryptBotPrivateKeys,
|
||||||
|
reencryptSecretBlindIndexDataSalts
|
||||||
|
} from './reencryptData';
|
||||||
|
import {
|
||||||
|
getNodeEnv,
|
||||||
|
getMongoURL,
|
||||||
|
getSentryDSN
|
||||||
|
} from '../../config';
|
||||||
|
import { initializePassport } from '../auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare Infisical upon startup. This includes tasks like:
|
||||||
|
* - Log initial telemetry message
|
||||||
|
* - Initializing SMTP configuration
|
||||||
|
* - Initializing the instance global feature set (if applicable)
|
||||||
|
* - Initializing the database connection
|
||||||
|
* - Initializing Sentry
|
||||||
|
* - Backfilling data
|
||||||
|
* - Re-encrypting data
|
||||||
|
*/
|
||||||
|
export const setup = async () => {
|
||||||
|
patchRouterParam();
|
||||||
|
await validateEncryptionKeysConfig();
|
||||||
|
await TelemetryService.logTelemetryMessage();
|
||||||
|
|
||||||
|
// initializing SMTP configuration
|
||||||
|
setTransporter(await initSmtp());
|
||||||
|
|
||||||
|
// initializing global feature set
|
||||||
|
await EELicenseService.initGlobalFeatureSet();
|
||||||
|
|
||||||
|
// initializing the database connection
|
||||||
|
await DatabaseService.initDatabase(await getMongoURL());
|
||||||
|
await initializePassport();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: the order in this setup function is critical.
|
||||||
|
* It is important to backfill data before performing any re-encryption functionality.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// backfilling data to catch up with new collections and updated fields
|
||||||
|
await backfillSecretVersions();
|
||||||
|
await backfillBots();
|
||||||
|
await backfillSecretBlindIndexData();
|
||||||
|
await backfillEncryptionMetadata();
|
||||||
|
|
||||||
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
await reencryptBotPrivateKeys();
|
||||||
|
await reencryptSecretBlindIndexDataSalts();
|
||||||
|
|
||||||
|
// initializing Sentry
|
||||||
|
Sentry.init({
|
||||||
|
dsn: await getSentryDSN(),
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
debug: (await getNodeEnv()) === 'production' ? false : true,
|
||||||
|
environment: (await getNodeEnv())
|
||||||
|
});
|
||||||
|
|
||||||
|
await createTestUserForDevelopment();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
IBot,
|
||||||
|
ISecretBlindIndexData,
|
||||||
|
SecretBlindIndexData
|
||||||
|
} from '../../models';
|
||||||
|
import { decryptSymmetric128BitHexKeyUTF8 } from '../../utils/crypto';
|
||||||
|
import {
|
||||||
|
client,
|
||||||
|
getEncryptionKey,
|
||||||
|
getRootEncryptionKey
|
||||||
|
} from '../../config';
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY
|
||||||
|
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
*/
|
||||||
|
export const reencryptBotPrivateKeys = async () => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
if (encryptionKey && rootEncryptionKey) {
|
||||||
|
// 1: re-encrypt bot private keys under ROOT_ENCRYPTION_KEY
|
||||||
|
const bots = await Bot.find({
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).select('+encryptedPrivateKey iv tag algorithm keyEncoding');
|
||||||
|
|
||||||
|
if (bots.length === 0) return;
|
||||||
|
|
||||||
|
const operationsBot = await Promise.all(
|
||||||
|
bots.map(async (bot: IBot) => {
|
||||||
|
|
||||||
|
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
_id: bot._id
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Bot.bulkWrite(operationsBot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
|
||||||
|
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
*/
|
||||||
|
export const reencryptSecretBlindIndexDataSalts = async () => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
if (encryptionKey && rootEncryptionKey) {
|
||||||
|
const secretBlindIndexData = await SecretBlindIndexData.find({
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).select('+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding');
|
||||||
|
|
||||||
|
if (secretBlindIndexData.length == 0) return;
|
||||||
|
|
||||||
|
const operationsSecretBlindIndexData = await Promise.all(
|
||||||
|
secretBlindIndexData.map(async (secretBlindIndexDatum: ISecretBlindIndexData) => {
|
||||||
|
|
||||||
|
const salt = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: secretBlindIndexDatum.encryptedSaltCiphertext,
|
||||||
|
iv: secretBlindIndexDatum.saltIV,
|
||||||
|
tag: secretBlindIndexDatum.saltTag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSaltCiphertext,
|
||||||
|
iv: saltIV,
|
||||||
|
tag: saltTag
|
||||||
|
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||||
|
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
_id: secretBlindIndexDatum._id
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
encryptedSaltCiphertext,
|
||||||
|
saltIV,
|
||||||
|
saltTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await SecretBlindIndexData.bulkWrite(operationsSecretBlindIndexData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
getEncryptionKey,
|
||||||
|
getRootEncryptionKey
|
||||||
|
} from '../../config';
|
||||||
|
import {
|
||||||
|
InternalServerError
|
||||||
|
} from '../../utils/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY. Specifically:
|
||||||
|
* - ENCRYPTION_KEY is a hex, 128-bit string
|
||||||
|
* - ROOT_ENCRYPTION_KEY is a base64, 128-bit string
|
||||||
|
* - Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY are present
|
||||||
|
*
|
||||||
|
* - Encrypted data is consistent with the passed in encryption keys
|
||||||
|
*
|
||||||
|
* NOTE 1: ENCRYPTION_KEY is being transitioned to ROOT_ENCRYPTION_KEY
|
||||||
|
* NOTE 2: In the future, we will have a superior validation function
|
||||||
|
* built into the SDK.
|
||||||
|
*/
|
||||||
|
export const validateEncryptionKeysConfig = async () => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(encryptionKey === undefined || encryptionKey === "") &&
|
||||||
|
(rootEncryptionKey === undefined || rootEncryptionKey === "")
|
||||||
|
) throw InternalServerError({
|
||||||
|
message: "Failed to find required root encryption key environment variable. Please make sure that you're passing in a ROOT_ENCRYPTION_KEY environment variable."
|
||||||
|
});
|
||||||
|
|
||||||
|
if (encryptionKey && encryptionKey !== '') {
|
||||||
|
// validate [encryptionKey]
|
||||||
|
|
||||||
|
const keyBuffer = Buffer.from(encryptionKey, 'hex');
|
||||||
|
const decoded = keyBuffer.toString('hex');
|
||||||
|
|
||||||
|
if (decoded !== encryptionKey) throw InternalServerError({
|
||||||
|
message: 'Failed to validate that the encryption key is correctly encoded in hex.'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keyBuffer.length !== 16) throw InternalServerError({
|
||||||
|
message: 'Failed to validate that the encryption key is a 128-bit hex string.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootEncryptionKey && rootEncryptionKey !== '') {
|
||||||
|
// validate [rootEncryptionKey]
|
||||||
|
|
||||||
|
const keyBuffer = Buffer.from(rootEncryptionKey, 'base64')
|
||||||
|
const decoded = keyBuffer.toString('base64');
|
||||||
|
|
||||||
|
if (decoded !== rootEncryptionKey) throw InternalServerError({
|
||||||
|
message: 'Failed to validate that the root encryption key is correctly encoded in base64'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keyBuffer.length !== 32) throw InternalServerError({
|
||||||
|
message: 'Failed to validate that the encryption key is a 256-bit base64 string'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
Bot,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||||
|
import { validateUserClientForWorkspace } from './user';
|
||||||
|
import {
|
||||||
|
UnauthorizedRequestError,
|
||||||
|
BotNotFoundError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for bot with id [botId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.botId - id of bot to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
|
*/
|
||||||
|
export const validateClientForBot = async ({
|
||||||
|
authData,
|
||||||
|
botId,
|
||||||
|
acceptedRoles,
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
botId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<"admin" | "member">;
|
||||||
|
}) => {
|
||||||
|
const bot = await Bot.findById(botId);
|
||||||
|
|
||||||
|
if (!bot) throw BotNotFoundError();
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_JWT &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
|
) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
acceptedRoles,
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||||
|
authData.authPayload instanceof ServiceAccount
|
||||||
|
) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
||||||
|
authData.authPayload instanceof ServiceTokenData
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for bot",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_API_KEY &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
|
) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
acceptedRoles,
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw BotNotFoundError({
|
||||||
|
message: "Failed client authorization for bot",
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export * from './workspace';
|
||||||
|
export * from './bot';
|
||||||
|
export * from './integration';
|
||||||
|
export * from './integrationAuth';
|
||||||
|
export * from './membership';
|
||||||
|
export * from './membershipOrg';
|
||||||
|
export * from './organization';
|
||||||
|
export * from './secrets';
|
||||||
|
export * from './serviceAccount';
|
||||||
|
export * from './serviceTokenData';
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||||
|
import { validateUserClientForWorkspace } from './user';
|
||||||
|
import { IntegrationService } from '../services';
|
||||||
|
import {
|
||||||
|
IntegrationNotFoundError,
|
||||||
|
IntegrationAuthNotFoundError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for integration with id [integrationId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.integrationId - id of integration to validate against
|
||||||
|
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateClientForIntegration = async ({
|
||||||
|
authData,
|
||||||
|
integrationId,
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
integrationId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const integration = await Integration.findById(integrationId);
|
||||||
|
if (!integration) throw IntegrationNotFoundError();
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integration.integrationAuth)
|
||||||
|
.select(
|
||||||
|
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw IntegrationAuthNotFoundError();
|
||||||
|
|
||||||
|
const accessToken = (await IntegrationService.getIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id
|
||||||
|
})).accessToken;
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ integration, accessToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: integration.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ integration, accessToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service token authorization for integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ integration, accessToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@ import {
|
|||||||
UnauthorizedRequestError
|
UnauthorizedRequestError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import { IntegrationService } from '../services';
|
import { IntegrationService } from '../services';
|
||||||
import { validateUserClientForWorkspace } from '../helpers/user';
|
import { validateUserClientForWorkspace } from './user';
|
||||||
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
|
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
Membership,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||||
|
import { validateUserClientForWorkspace } from './user';
|
||||||
|
import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData';
|
||||||
|
import {
|
||||||
|
MembershipNotFoundError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for membership with id [membershipId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
|
||||||
|
* @returns {Membership} - validated membership
|
||||||
|
*/
|
||||||
|
export const validateClientForMembership = async ({
|
||||||
|
authData,
|
||||||
|
membershipId,
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
membershipId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const membership = await Membership.findById(membershipId);
|
||||||
|
|
||||||
|
if (!membership) throw MembershipNotFoundError({
|
||||||
|
message: 'Failed to find membership'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
await validateServiceTokenDataClientForWorkspace({
|
||||||
|
serviceTokenData: authData.authPayload,
|
||||||
|
workspaceId: new Types.ObjectId(membership.workspace)
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
MembershipOrg,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
validateMembershipOrg
|
||||||
|
} from '../helpers/membershipOrg';
|
||||||
|
import {
|
||||||
|
MembershipOrgNotFoundError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for organization membership with id [membershipOrgId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
|
||||||
|
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
|
||||||
|
* @param {MembershipOrg} - validated organization membership
|
||||||
|
*/
|
||||||
|
export const validateClientForMembershipOrg = async ({
|
||||||
|
authData,
|
||||||
|
membershipOrgId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
membershipOrgId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
|
}) => {
|
||||||
|
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
|
||||||
|
|
||||||
|
if (!membershipOrg) throw MembershipOrgNotFoundError({
|
||||||
|
message: 'Failed to find organization membership '
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateMembershipOrg({
|
||||||
|
userId: authData.authPayload._id,
|
||||||
|
organizationId: membershipOrg.organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service account client authorization for organization membership'
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service account client authorization for organization membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateMembershipOrg({
|
||||||
|
userId: authData.authPayload._id,
|
||||||
|
organizationId: membershipOrg.organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for organization membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
Organization,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
OrganizationNotFoundError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import { validateUserClientForOrganization } from './user';
|
||||||
|
import { validateServiceAccountClientForOrganization } from './serviceAccount';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate accepted clients for organization with id [organizationId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
|
||||||
|
*/
|
||||||
|
export const validateClientForOrganization = async ({
|
||||||
|
authData,
|
||||||
|
organizationId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
organizationId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<"owner" | "admin" | "member">;
|
||||||
|
acceptedStatuses: Array<"invited" | "accepted">;
|
||||||
|
}) => {
|
||||||
|
const organization = await Organization.findById(organizationId);
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
throw OrganizationNotFoundError({
|
||||||
|
message: "Failed to find organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_JWT &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
|
) {
|
||||||
|
const membershipOrg = await validateUserClientForOrganization({
|
||||||
|
user: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization, membershipOrg };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||||
|
authData.authPayload instanceof ServiceAccount
|
||||||
|
) {
|
||||||
|
await validateServiceAccountClientForOrganization({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
||||||
|
authData.authPayload instanceof ServiceTokenData
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_API_KEY &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
|
) {
|
||||||
|
const membershipOrg = await validateUserClientForOrganization({
|
||||||
|
user: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization, membershipOrg };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed client authorization for organization",
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ISecret,
|
||||||
|
Secret,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
|
import { validateServiceAccountClientForWorkspace, validateServiceAccountClientForSecrets } from './serviceAccount';
|
||||||
|
import { validateUserClientForSecret, validateUserClientForSecrets } from './user';
|
||||||
|
import { validateServiceTokenDataClientForWorkspace, validateServiceTokenDataClientForSecrets } from './serviceTokenData';
|
||||||
|
import { AuthData } from '../interfaces/middleware';
|
||||||
|
import {
|
||||||
|
SecretNotFoundError,
|
||||||
|
UnauthorizedRequestError,
|
||||||
|
BadRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for secrets with id [secretId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateClientForSecret = async ({
|
||||||
|
authData,
|
||||||
|
secretId,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
}: {
|
||||||
|
authData: AuthData;
|
||||||
|
secretId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
requiredPermissions: string[];
|
||||||
|
}) => {
|
||||||
|
const secret = await Secret.findById(secretId);
|
||||||
|
|
||||||
|
if (!secret) throw SecretNotFoundError({
|
||||||
|
message: 'Failed to find secret'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForSecret({
|
||||||
|
user: authData.authPayload,
|
||||||
|
secret,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: secret.workspace,
|
||||||
|
environment: secret.environment,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
await validateServiceTokenDataClientForWorkspace({
|
||||||
|
serviceTokenData: authData.authPayload,
|
||||||
|
workspaceId: secret.workspace,
|
||||||
|
environment: secret.environment
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForSecret({
|
||||||
|
user: authData.authPayload,
|
||||||
|
secret,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for secret'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for secrets with ids [secretIds] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
|
||||||
|
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateClientForSecrets = async ({
|
||||||
|
authData,
|
||||||
|
secretIds,
|
||||||
|
requiredPermissions
|
||||||
|
}: {
|
||||||
|
authData: AuthData;
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
requiredPermissions: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let secrets: ISecret[] = [];
|
||||||
|
|
||||||
|
secrets = await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: secretIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secrets.length != secretIds.length) {
|
||||||
|
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForSecrets({
|
||||||
|
user: authData.authPayload,
|
||||||
|
secrets,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForSecrets({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
secrets,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
await validateServiceTokenDataClientForSecrets({
|
||||||
|
serviceTokenData: authData.authPayload,
|
||||||
|
secrets,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForSecrets({
|
||||||
|
user: authData.authPayload,
|
||||||
|
secrets,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for secrets resource'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
ISecret,
|
ISecret,
|
||||||
IOrganization,
|
IOrganization,
|
||||||
IServiceAccountWorkspacePermission,
|
|
||||||
ServiceAccountWorkspacePermission
|
ServiceAccountWorkspacePermission
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import { validateUserClientForServiceAccount } from './user';
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
UnauthorizedRequestError,
|
UnauthorizedRequestError,
|
||||||
@@ -25,11 +25,8 @@ import {
|
|||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
|
||||||
validateUserClientForServiceAccount
|
|
||||||
} from '../helpers/user';
|
|
||||||
|
|
||||||
const validateClientForServiceAccount = async ({
|
export const validateClientForServiceAccount = async ({
|
||||||
authData,
|
authData,
|
||||||
serviceAccountId,
|
serviceAccountId,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
@@ -100,7 +97,7 @@ const validateClientForServiceAccount = async ({
|
|||||||
* @param {String} environment - (optional) environment in workspace to validate against
|
* @param {String} environment - (optional) environment in workspace to validate against
|
||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateServiceAccountClientForWorkspace = async ({
|
export const validateServiceAccountClientForWorkspace = async ({
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -169,7 +166,7 @@ const validateClientForServiceAccount = async ({
|
|||||||
* @param {Secret[]} secrets - secrets to validate against
|
* @param {Secret[]} secrets - secrets to validate against
|
||||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateServiceAccountClientForSecrets = async ({
|
export const validateServiceAccountClientForSecrets = async ({
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
secrets,
|
secrets,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
@@ -226,7 +223,7 @@ const validateClientForServiceAccount = async ({
|
|||||||
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
|
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
|
||||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateServiceAccountClientForServiceAccount = ({
|
export const validateServiceAccountClientForServiceAccount = ({
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
targetServiceAccount,
|
targetServiceAccount,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
@@ -248,7 +245,7 @@ const validateServiceAccountClientForServiceAccount = ({
|
|||||||
* @param {User} obj.user - service account client
|
* @param {User} obj.user - service account client
|
||||||
* @param {Organization} obj.organization - organization to validate against
|
* @param {Organization} obj.organization - organization to validate against
|
||||||
*/
|
*/
|
||||||
const validateServiceAccountClientForOrganization = async ({
|
export const validateServiceAccountClientForOrganization = async ({
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
organization
|
organization
|
||||||
}: {
|
}: {
|
||||||
@@ -260,12 +257,4 @@ const validateServiceAccountClientForOrganization = async ({
|
|||||||
message: 'Failed service account authorization for the given organization'
|
message: 'Failed service account authorization for the given organization'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
validateClientForServiceAccount,
|
|
||||||
validateServiceAccountClientForWorkspace,
|
|
||||||
validateServiceAccountClientForSecrets,
|
|
||||||
validateServiceAccountClientForServiceAccount,
|
|
||||||
validateServiceAccountClientForOrganization
|
|
||||||
}
|
}
|
||||||
+5
-11
@@ -18,8 +18,8 @@ import {
|
|||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import { validateUserClientForWorkspace } from '../helpers/user';
|
import { validateUserClientForWorkspace } from './user';
|
||||||
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authenticated clients for service token with id [serviceTokenId] based
|
* Validate authenticated clients for service token with id [serviceTokenId] based
|
||||||
@@ -29,7 +29,7 @@ import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAcco
|
|||||||
* @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
|
* @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
|
||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
*/
|
*/
|
||||||
const validateClientForServiceTokenData = async ({
|
export const validateClientForServiceTokenData = async ({
|
||||||
authData,
|
authData,
|
||||||
serviceTokenDataId,
|
serviceTokenDataId,
|
||||||
acceptedRoles
|
acceptedRoles
|
||||||
@@ -100,7 +100,7 @@ const validateClientForServiceTokenData = async ({
|
|||||||
* @param {String} environment - (optional) environment in workspace to validate against
|
* @param {String} environment - (optional) environment in workspace to validate against
|
||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateServiceTokenDataClientForWorkspace = async ({
|
export const validateServiceTokenDataClientForWorkspace = async ({
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -146,7 +146,7 @@ const validateClientForServiceTokenData = async ({
|
|||||||
* @param {Secret[]} secrets - secrets to validate against
|
* @param {Secret[]} secrets - secrets to validate against
|
||||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateServiceTokenDataClientForSecrets = async ({
|
export const validateServiceTokenDataClientForSecrets = async ({
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
secrets,
|
secrets,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
@@ -179,10 +179,4 @@ const validateClientForServiceTokenData = async ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
validateClientForServiceTokenData,
|
|
||||||
validateServiceTokenDataClientForWorkspace,
|
|
||||||
validateServiceTokenDataClientForSecrets
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
ISecret,
|
||||||
|
IServiceAccount,
|
||||||
|
Membership,
|
||||||
|
IOrganization,
|
||||||
|
} from '../models';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
import {
|
||||||
|
validateMembershipOrg
|
||||||
|
} from '../helpers/membershipOrg';
|
||||||
|
import {
|
||||||
|
PERMISSION_READ_SECRETS,
|
||||||
|
PERMISSION_WRITE_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user (client) can access workspace
|
||||||
|
* with id [workspaceId] and its environment [environment] with required permissions
|
||||||
|
* [requiredPermissions]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {User} obj.user - user client
|
||||||
|
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||||
|
* @param {String} environment - (optional) environment in workspace to validate against
|
||||||
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateUserClientForWorkspace = async ({
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment?: string;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
requiredPermissions?: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// validate user membership in workspace
|
||||||
|
const membership = await validateMembership({
|
||||||
|
userId: user._id,
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
let runningIsDisallowed = false;
|
||||||
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
|
switch (requiredPermission) {
|
||||||
|
case PERMISSION_READ_SECRETS:
|
||||||
|
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||||
|
break;
|
||||||
|
case PERMISSION_WRITE_SECRETS:
|
||||||
|
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runningIsDisallowed) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user (client) can access secret [secret]
|
||||||
|
* with required permissions [requiredPermissions]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {User} obj.user - user client
|
||||||
|
* @param {Secret[]} obj.secrets - secrets to validate against
|
||||||
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateUserClientForSecret = async ({
|
||||||
|
user,
|
||||||
|
secret,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
secret: ISecret;
|
||||||
|
acceptedRoles?: Array<'admin' | 'member'>;
|
||||||
|
requiredPermissions?: string[];
|
||||||
|
}) => {
|
||||||
|
const membership = await validateMembership({
|
||||||
|
userId: user._id,
|
||||||
|
workspaceId: secret.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
|
||||||
|
const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
|
||||||
|
|
||||||
|
if (isDisallowed) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'You do not have the required permissions to perform this action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user (client) can access secrets [secrets]
|
||||||
|
* with required permissions [requiredPermissions]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {User} obj.user - user client
|
||||||
|
* @param {Secret[]} obj.secrets - secrets to validate against
|
||||||
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateUserClientForSecrets = async ({
|
||||||
|
user,
|
||||||
|
secrets,
|
||||||
|
requiredPermissions
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
secrets: ISecret[];
|
||||||
|
requiredPermissions?: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// TODO: add acceptedRoles?
|
||||||
|
|
||||||
|
const userMemberships = await Membership.find({ user: user._id })
|
||||||
|
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
||||||
|
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
|
||||||
|
|
||||||
|
// for each secret check if the secret belongs to a workspace the user is a member of
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
if (!workspaceIdsSet.has(secret.workspace.toString())) {
|
||||||
|
throw BadRequestError({
|
||||||
|
message: 'Failed authorization for the secret'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
|
||||||
|
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
|
||||||
|
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
|
||||||
|
|
||||||
|
if (isDisallowed) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'You do not have the required permissions to perform this action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user (client) can access service account [serviceAccount]
|
||||||
|
* with required permissions [requiredPermissions]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {User} obj.user - user client
|
||||||
|
* @param {ServiceAccount} obj.serviceAccount - service account to validate against
|
||||||
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateUserClientForServiceAccount = async ({
|
||||||
|
user,
|
||||||
|
serviceAccount,
|
||||||
|
requiredPermissions
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
serviceAccount: IServiceAccount;
|
||||||
|
requiredPermissions?: string[];
|
||||||
|
}) => {
|
||||||
|
if (!serviceAccount.user.equals(user._id)) {
|
||||||
|
// case: user who created service account is not the
|
||||||
|
// same user that is on the request
|
||||||
|
await validateMembershipOrg({
|
||||||
|
userId: user._id,
|
||||||
|
organizationId: serviceAccount.organization,
|
||||||
|
acceptedRoles: [],
|
||||||
|
acceptedStatuses: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user (client) can access organization [organization]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {User} obj.user - user client
|
||||||
|
* @param {Organization} obj.organization - organization to validate against
|
||||||
|
*/
|
||||||
|
export const validateUserClientForOrganization = async ({
|
||||||
|
user,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
organization: IOrganization;
|
||||||
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
|
}) => {
|
||||||
|
const membershipOrg = await validateMembershipOrg({
|
||||||
|
userId: user._id,
|
||||||
|
organizationId: organization._id,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
IUser,
|
||||||
|
IServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
Workspace,
|
||||||
|
User,
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData,
|
||||||
|
SecretBlindIndexData
|
||||||
|
} from '../models';
|
||||||
|
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||||
|
import { validateUserClientForWorkspace } from './user';
|
||||||
|
import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData';
|
||||||
|
import {
|
||||||
|
UnauthorizedRequestError,
|
||||||
|
WorkspaceNotFoundError
|
||||||
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for workspace with id [workspaceId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||||
|
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||||
|
*/
|
||||||
|
export const validateClientForWorkspace = async ({
|
||||||
|
authData,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions,
|
||||||
|
requireBlindIndicesEnabled
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
environment?: string;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
requiredPermissions?: string[];
|
||||||
|
requireBlindIndicesEnabled: boolean;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const workspace = await Workspace.findById(workspaceId);
|
||||||
|
|
||||||
|
if (!workspace) throw WorkspaceNotFoundError({
|
||||||
|
message: 'Failed to find workspace'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requireBlindIndicesEnabled) {
|
||||||
|
// case: blind indices are not enabled for secrets in this workspace
|
||||||
|
// (i.e. workspace was created before blind indices were introduced
|
||||||
|
// and no admin has enabled it)
|
||||||
|
|
||||||
|
const secretBlindIndexData = await SecretBlindIndexData.exists({
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!secretBlindIndexData) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed workspace authorization due to blind indices not being enabled'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
const membership = await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ membership, workspace });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
await validateServiceTokenDataClientForWorkspace({
|
||||||
|
serviceTokenData: authData.authPayload,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
const membership = await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
|
requiredPermissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ membership, workspace });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
const ACTION_LOGIN = 'login';
|
export const ACTION_LOGIN = 'login';
|
||||||
const ACTION_LOGOUT = 'logout';
|
export const ACTION_LOGOUT = 'logout';
|
||||||
const ACTION_ADD_SECRETS = 'addSecrets';
|
export const ACTION_ADD_SECRETS = 'addSecrets';
|
||||||
const ACTION_DELETE_SECRETS = 'deleteSecrets';
|
export const ACTION_DELETE_SECRETS = 'deleteSecrets';
|
||||||
const ACTION_UPDATE_SECRETS = 'updateSecrets';
|
export const ACTION_UPDATE_SECRETS = 'updateSecrets';
|
||||||
const ACTION_READ_SECRETS = 'readSecrets';
|
export const ACTION_READ_SECRETS = 'readSecrets';
|
||||||
|
|
||||||
export {
|
|
||||||
ACTION_LOGIN,
|
|
||||||
ACTION_LOGOUT,
|
|
||||||
ACTION_ADD_SECRETS,
|
|
||||||
ACTION_DELETE_SECRETS,
|
|
||||||
ACTION_UPDATE_SECRETS,
|
|
||||||
ACTION_READ_SECRETS
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
const AUTH_MODE_JWT = 'jwt';
|
export const AUTH_MODE_JWT = 'jwt';
|
||||||
const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
|
export const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
|
||||||
const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
|
export const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
|
||||||
const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
|
export const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
|
||||||
|
|
||||||
export {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const ALGORITHM_AES_256_GCM = 'aes-256-gcm';
|
||||||
|
export const NONCE_BYTES_SIZE = 12;
|
||||||
|
export const BLOCK_SIZE_BYTES_16 = 16;
|
||||||
|
|
||||||
|
export const ENCODING_SCHEME_UTF8 = 'utf8';
|
||||||
|
export const ENCODING_SCHEME_HEX = 'hex';
|
||||||
|
export const ENCODING_SCHEME_BASE64 = 'base64';
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
// environments
|
// environments
|
||||||
const ENV_DEV = 'dev';
|
export const ENV_DEV = 'dev';
|
||||||
const ENV_TESTING = 'test';
|
export const ENV_TESTING = 'test';
|
||||||
const ENV_STAGING = 'staging';
|
export const ENV_STAGING = 'staging';
|
||||||
const ENV_PROD = 'prod';
|
export const ENV_PROD = 'prod';
|
||||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
export const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||||
|
|
||||||
export {
|
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD,
|
|
||||||
ENV_SET
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,2 @@
|
|||||||
const EVENT_PUSH_SECRETS = 'pushSecrets';
|
export const EVENT_PUSH_SECRETS = 'pushSecrets';
|
||||||
const EVENT_PULL_SECRETS = 'pullSecrets';
|
export const EVENT_PULL_SECRETS = 'pullSecrets';
|
||||||
|
|
||||||
export {
|
|
||||||
EVENT_PUSH_SECRETS,
|
|
||||||
EVENT_PULL_SECRETS
|
|
||||||
}
|
|
||||||
+13
-154
@@ -1,154 +1,13 @@
|
|||||||
import {
|
export * from './action';
|
||||||
ENV_DEV,
|
export * from './authentication';
|
||||||
ENV_TESTING,
|
export * from './crypto';
|
||||||
ENV_STAGING,
|
export * from './environment';
|
||||||
ENV_PROD,
|
export * from './event';
|
||||||
ENV_SET,
|
export * from './integration';
|
||||||
} from "./environment";
|
export * from './organization';
|
||||||
import {
|
export * from './permission';
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
export * from './secret';
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
export * from './smtp';
|
||||||
INTEGRATION_AWS_SECRET_MANAGER,
|
export * from './stripe';
|
||||||
INTEGRATION_HEROKU,
|
export * from './token';
|
||||||
INTEGRATION_VERCEL,
|
export * from './user';
|
||||||
INTEGRATION_NETLIFY,
|
|
||||||
INTEGRATION_GITHUB,
|
|
||||||
INTEGRATION_GITLAB,
|
|
||||||
INTEGRATION_RENDER,
|
|
||||||
INTEGRATION_RAILWAY,
|
|
||||||
INTEGRATION_FLYIO,
|
|
||||||
INTEGRATION_CIRCLECI,
|
|
||||||
INTEGRATION_TRAVISCI,
|
|
||||||
INTEGRATION_SUPABASE,
|
|
||||||
INTEGRATION_SET,
|
|
||||||
INTEGRATION_OAUTH2,
|
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
|
||||||
INTEGRATION_GITLAB_TOKEN_URL,
|
|
||||||
INTEGRATION_HEROKU_API_URL,
|
|
||||||
INTEGRATION_GITLAB_API_URL,
|
|
||||||
INTEGRATION_VERCEL_API_URL,
|
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
|
||||||
INTEGRATION_RENDER_API_URL,
|
|
||||||
INTEGRATION_RAILWAY_API_URL,
|
|
||||||
INTEGRATION_FLYIO_API_URL,
|
|
||||||
INTEGRATION_CIRCLECI_API_URL,
|
|
||||||
INTEGRATION_TRAVISCI_API_URL,
|
|
||||||
INTEGRATION_SUPABASE_API_URL,
|
|
||||||
getIntegrationOptions
|
|
||||||
} from "./integration";
|
|
||||||
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
|
|
||||||
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
|
|
||||||
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
|
|
||||||
import {
|
|
||||||
ACTION_LOGIN,
|
|
||||||
ACTION_LOGOUT,
|
|
||||||
ACTION_ADD_SECRETS,
|
|
||||||
ACTION_UPDATE_SECRETS,
|
|
||||||
ACTION_DELETE_SECRETS,
|
|
||||||
ACTION_READ_SECRETS
|
|
||||||
} from './action';
|
|
||||||
import {
|
|
||||||
SMTP_HOST_SENDGRID,
|
|
||||||
SMTP_HOST_MAILGUN,
|
|
||||||
SMTP_HOST_SOCKETLABS,
|
|
||||||
SMTP_HOST_ZOHOMAIL,
|
|
||||||
SMTP_HOST_GMAIL
|
|
||||||
} from './smtp';
|
|
||||||
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
|
||||||
import {
|
|
||||||
MFA_METHOD_EMAIL
|
|
||||||
} from './user';
|
|
||||||
import {
|
|
||||||
TOKEN_EMAIL_CONFIRMATION,
|
|
||||||
TOKEN_EMAIL_MFA,
|
|
||||||
TOKEN_EMAIL_ORG_INVITATION,
|
|
||||||
TOKEN_EMAIL_PASSWORD_RESET
|
|
||||||
} from './token';
|
|
||||||
import {
|
|
||||||
PERMISSION_READ_SECRETS,
|
|
||||||
PERMISSION_WRITE_SECRETS
|
|
||||||
} from './permission';
|
|
||||||
import {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY
|
|
||||||
} from './authentication';
|
|
||||||
|
|
||||||
export {
|
|
||||||
OWNER,
|
|
||||||
ADMIN,
|
|
||||||
MEMBER,
|
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
SECRET_SHARED,
|
|
||||||
SECRET_PERSONAL,
|
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD,
|
|
||||||
ENV_SET,
|
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
|
||||||
INTEGRATION_AWS_SECRET_MANAGER,
|
|
||||||
INTEGRATION_HEROKU,
|
|
||||||
INTEGRATION_VERCEL,
|
|
||||||
INTEGRATION_NETLIFY,
|
|
||||||
INTEGRATION_GITHUB,
|
|
||||||
INTEGRATION_GITLAB,
|
|
||||||
INTEGRATION_RENDER,
|
|
||||||
INTEGRATION_RAILWAY,
|
|
||||||
INTEGRATION_FLYIO,
|
|
||||||
INTEGRATION_CIRCLECI,
|
|
||||||
INTEGRATION_TRAVISCI,
|
|
||||||
INTEGRATION_SUPABASE,
|
|
||||||
INTEGRATION_SET,
|
|
||||||
INTEGRATION_OAUTH2,
|
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
|
||||||
INTEGRATION_GITLAB_TOKEN_URL,
|
|
||||||
INTEGRATION_HEROKU_API_URL,
|
|
||||||
INTEGRATION_GITLAB_API_URL,
|
|
||||||
INTEGRATION_VERCEL_API_URL,
|
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
|
||||||
INTEGRATION_RENDER_API_URL,
|
|
||||||
INTEGRATION_RAILWAY_API_URL,
|
|
||||||
INTEGRATION_FLYIO_API_URL,
|
|
||||||
INTEGRATION_CIRCLECI_API_URL,
|
|
||||||
INTEGRATION_TRAVISCI_API_URL,
|
|
||||||
INTEGRATION_SUPABASE_API_URL,
|
|
||||||
EVENT_PUSH_SECRETS,
|
|
||||||
EVENT_PULL_SECRETS,
|
|
||||||
ACTION_LOGIN,
|
|
||||||
ACTION_LOGOUT,
|
|
||||||
ACTION_ADD_SECRETS,
|
|
||||||
ACTION_UPDATE_SECRETS,
|
|
||||||
ACTION_DELETE_SECRETS,
|
|
||||||
ACTION_READ_SECRETS,
|
|
||||||
PERMISSION_READ_SECRETS,
|
|
||||||
PERMISSION_WRITE_SECRETS,
|
|
||||||
getIntegrationOptions,
|
|
||||||
SMTP_HOST_SENDGRID,
|
|
||||||
SMTP_HOST_MAILGUN,
|
|
||||||
SMTP_HOST_SOCKETLABS,
|
|
||||||
SMTP_HOST_ZOHOMAIL,
|
|
||||||
SMTP_HOST_GMAIL,
|
|
||||||
PLAN_STARTER,
|
|
||||||
PLAN_PRO,
|
|
||||||
MFA_METHOD_EMAIL,
|
|
||||||
TOKEN_EMAIL_CONFIRMATION,
|
|
||||||
TOKEN_EMAIL_MFA,
|
|
||||||
TOKEN_EMAIL_ORG_INVITATION,
|
|
||||||
TOKEN_EMAIL_PASSWORD_RESET,
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ import {
|
|||||||
} from '../config';
|
} from '../config';
|
||||||
|
|
||||||
// integrations
|
// integrations
|
||||||
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
export const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
||||||
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
export const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
||||||
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
export const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
||||||
const INTEGRATION_HEROKU = "heroku";
|
export const INTEGRATION_HEROKU = "heroku";
|
||||||
const INTEGRATION_VERCEL = "vercel";
|
export const INTEGRATION_VERCEL = "vercel";
|
||||||
const INTEGRATION_NETLIFY = "netlify";
|
export const INTEGRATION_NETLIFY = "netlify";
|
||||||
const INTEGRATION_GITHUB = "github";
|
export const INTEGRATION_GITHUB = "github";
|
||||||
const INTEGRATION_GITLAB = "gitlab";
|
export const INTEGRATION_GITLAB = "gitlab";
|
||||||
const INTEGRATION_RENDER = "render";
|
export const INTEGRATION_RENDER = "render";
|
||||||
const INTEGRATION_RAILWAY = "railway";
|
export const INTEGRATION_RAILWAY = "railway";
|
||||||
const INTEGRATION_FLYIO = "flyio";
|
export const INTEGRATION_FLYIO = "flyio";
|
||||||
const INTEGRATION_CIRCLECI = "circleci";
|
export const INTEGRATION_CIRCLECI = "circleci";
|
||||||
const INTEGRATION_TRAVISCI = "travisci";
|
export const INTEGRATION_TRAVISCI = "travisci";
|
||||||
const INTEGRATION_SUPABASE = 'supabase';
|
export const INTEGRATION_SUPABASE = 'supabase';
|
||||||
const INTEGRATION_SET = new Set([
|
export const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
@@ -37,31 +37,31 @@ const INTEGRATION_SET = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// integration types
|
// integration types
|
||||||
const INTEGRATION_OAUTH2 = "oauth2";
|
export const INTEGRATION_OAUTH2 = "oauth2";
|
||||||
|
|
||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`;
|
export 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';
|
export const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
export const INTEGRATION_VERCEL_TOKEN_URL =
|
||||||
"https://api.vercel.com/v2/oauth/access_token";
|
"https://api.vercel.com/v2/oauth/access_token";
|
||||||
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
|
export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
|
||||||
const INTEGRATION_GITHUB_TOKEN_URL =
|
export const INTEGRATION_GITHUB_TOKEN_URL =
|
||||||
"https://github.com/login/oauth/access_token";
|
"https://github.com/login/oauth/access_token";
|
||||||
const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
|
export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
|
||||||
|
|
||||||
// integration apps endpoints
|
// integration apps endpoints
|
||||||
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
|
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
|
||||||
const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
|
export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
|
||||||
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
|
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
|
||||||
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
|
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
|
||||||
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
|
export const INTEGRATION_RENDER_API_URL = "https://api.render.com";
|
||||||
const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
|
export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
|
||||||
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||||
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||||
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
||||||
const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
|
export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
|
||||||
|
|
||||||
const getIntegrationOptions = async () => {
|
export const getIntegrationOptions = async () => {
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
{
|
{
|
||||||
name: 'Heroku',
|
name: 'Heroku',
|
||||||
@@ -202,41 +202,4 @@ const getIntegrationOptions = async () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return INTEGRATION_OPTIONS;
|
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_GITLAB,
|
|
||||||
INTEGRATION_RENDER,
|
|
||||||
INTEGRATION_RAILWAY,
|
|
||||||
INTEGRATION_FLYIO,
|
|
||||||
INTEGRATION_CIRCLECI,
|
|
||||||
INTEGRATION_TRAVISCI,
|
|
||||||
INTEGRATION_SUPABASE,
|
|
||||||
INTEGRATION_SET,
|
|
||||||
INTEGRATION_OAUTH2,
|
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
|
||||||
INTEGRATION_GITLAB_API_URL,
|
|
||||||
INTEGRATION_HEROKU_API_URL,
|
|
||||||
INTEGRATION_GITLAB_TOKEN_URL,
|
|
||||||
INTEGRATION_VERCEL_API_URL,
|
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
|
||||||
INTEGRATION_RENDER_API_URL,
|
|
||||||
INTEGRATION_RAILWAY_API_URL,
|
|
||||||
INTEGRATION_FLYIO_API_URL,
|
|
||||||
INTEGRATION_CIRCLECI_API_URL,
|
|
||||||
INTEGRATION_TRAVISCI_API_URL,
|
|
||||||
INTEGRATION_SUPABASE_API_URL,
|
|
||||||
getIntegrationOptions
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
// membership roles
|
// membership roles
|
||||||
const OWNER = "owner";
|
export const OWNER = "owner";
|
||||||
const ADMIN = "admin";
|
export const ADMIN = "admin";
|
||||||
const MEMBER = "member";
|
export const MEMBER = "member";
|
||||||
|
|
||||||
// membership statuses
|
// membership statuses
|
||||||
const INVITED = "invited";
|
export const INVITED = "invited";
|
||||||
|
|
||||||
// -- organization
|
// -- organization
|
||||||
const ACCEPTED = "accepted";
|
export const ACCEPTED = "accepted";
|
||||||
|
|
||||||
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED };
|
|
||||||
@@ -1,7 +1,2 @@
|
|||||||
const PERMISSION_READ_SECRETS = 'read';
|
export const PERMISSION_READ_SECRETS = 'read';
|
||||||
const PERMISSION_WRITE_SECRETS = 'write';
|
export const PERMISSION_WRITE_SECRETS = 'write';
|
||||||
|
|
||||||
export {
|
|
||||||
PERMISSION_READ_SECRETS,
|
|
||||||
PERMISSION_WRITE_SECRETS
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
// secrets
|
// secrets
|
||||||
const SECRET_SHARED = 'shared';
|
export const SECRET_SHARED = 'shared';
|
||||||
const SECRET_PERSONAL = 'personal';
|
export const SECRET_PERSONAL = 'personal';
|
||||||
|
|
||||||
export {
|
|
||||||
SECRET_SHARED,
|
|
||||||
SECRET_PERSONAL
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
|
export const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
|
||||||
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
|
export const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
|
||||||
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
|
export const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
|
||||||
const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
|
export const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
|
||||||
const SMTP_HOST_GMAIL = 'smtp.gmail.com';
|
export const SMTP_HOST_GMAIL = 'smtp.gmail.com';
|
||||||
|
|
||||||
export {
|
|
||||||
SMTP_HOST_SENDGRID,
|
|
||||||
SMTP_HOST_MAILGUN,
|
|
||||||
SMTP_HOST_SOCKETLABS,
|
|
||||||
SMTP_HOST_ZOHOMAIL,
|
|
||||||
SMTP_HOST_GMAIL
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
const PLAN_STARTER = 'starter';
|
export const PLAN_STARTER = 'starter';
|
||||||
const PLAN_PRO = 'pro';
|
export const PLAN_PRO = 'pro';
|
||||||
|
|
||||||
export {
|
|
||||||
PLAN_STARTER,
|
|
||||||
PLAN_PRO
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
|
export const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
|
||||||
const TOKEN_EMAIL_MFA = 'emailMfa';
|
export const TOKEN_EMAIL_MFA = 'emailMfa';
|
||||||
const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
|
export const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
|
||||||
const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
|
export const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
|
||||||
|
|
||||||
export {
|
|
||||||
TOKEN_EMAIL_CONFIRMATION,
|
|
||||||
TOKEN_EMAIL_MFA,
|
|
||||||
TOKEN_EMAIL_ORG_INVITATION,
|
|
||||||
TOKEN_EMAIL_PASSWORD_RESET
|
|
||||||
}
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
const MFA_METHOD_EMAIL = 'email';
|
export const MFA_METHOD_EMAIL = 'email';
|
||||||
|
|
||||||
export {
|
|
||||||
MFA_METHOD_EMAIL
|
|
||||||
}
|
|
||||||
@@ -7,4 +7,6 @@ process.env.NODE_ENV = 'test';
|
|||||||
process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392"
|
process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392"
|
||||||
process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364"
|
process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364"
|
||||||
process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2"
|
process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2"
|
||||||
process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5"
|
process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5"
|
||||||
|
process.env.ENCRYPTION_KEY="e05f54dffd58b5ab9b09e4c6fca7aff7"
|
||||||
|
process.env.ROOT_ENCRYPTION_KEY="MJA3DWJXjHiL6xjkUI2QCQuy/D+/SAbRNU1+rEo9gvQ="
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const jsrp = require('jsrp');
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser";
|
import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser";
|
||||||
import { encryptSymmetric } from "../../src/utils/crypto";
|
import {
|
||||||
|
encryptSymmetric128BitHexKeyUTF8
|
||||||
|
} from '../../src/utils/crypto';
|
||||||
|
|
||||||
interface TokenData {
|
interface TokenData {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -64,7 +66,7 @@ export const getJWTFromTestUser = (): Promise<TokenData> => {
|
|||||||
export const getServiceTokenFromTestUser = async () => {
|
export const getServiceTokenFromTestUser = async () => {
|
||||||
const loggedInUserDetails = await getJWTFromTestUser()
|
const loggedInUserDetails = await getJWTFromTestUser()
|
||||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: plainTextWorkspaceKey,
|
plaintext: plainTextWorkspaceKey,
|
||||||
key: randomBytes,
|
key: randomBytes,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { describe, test, expect } from '@jest/globals';
|
import { describe, test, expect } from '@jest/globals';
|
||||||
import {
|
import {
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
decryptSymmetric,
|
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric
|
|
||||||
} from '../../../src/utils/crypto';
|
} from '../../../src/utils/crypto';
|
||||||
|
|
||||||
describe('Crypto', () => {
|
describe('Crypto', () => {
|
||||||
@@ -153,99 +151,4 @@ describe('Crypto', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('encryptSymmetric', () => {
|
|
||||||
let plaintext: string;
|
|
||||||
const key = '7e8ee7e5cc667b9c1829783ad31f36f4';
|
|
||||||
|
|
||||||
test('should encrypt plaintext with the given key', () => {
|
|
||||||
plaintext = 'secret-message';
|
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key });
|
|
||||||
expect(ciphertext).toBeDefined();
|
|
||||||
expect(iv).toBeDefined();
|
|
||||||
expect(tag).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw an error when plaintext is undefined', () => {
|
|
||||||
const invalidKey = 'invalid-key';
|
|
||||||
expect(() => {
|
|
||||||
encryptSymmetric({ plaintext, key: invalidKey });
|
|
||||||
}).toThrowError('Invalid key length');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw an error when invalid key is provided', () => {
|
|
||||||
plaintext = 'secret-message';
|
|
||||||
const invalidKey = 'invalid-key';
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
encryptSymmetric({ plaintext, key: invalidKey });
|
|
||||||
}).toThrowError('Invalid key length');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('decryptSymmetric', () => {
|
|
||||||
const plaintext = 'secret-message';
|
|
||||||
const key = '7e8ee7e5cc667b9c1829783ad31f36f4';
|
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key });
|
|
||||||
|
|
||||||
test('should decrypt encrypted plaintext', () => {
|
|
||||||
const result = decryptSymmetric({
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result).toEqual(plaintext);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should fail if ciphertext is modified', () => {
|
|
||||||
const modifieldCiphertext = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
expect(() => {
|
|
||||||
decryptSymmetric({
|
|
||||||
ciphertext: modifieldCiphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
}).toThrowError('Unsupported state or unable to authenticate data');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should fail if iv is modified', () => {
|
|
||||||
const modifiedIv = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
expect(() => {
|
|
||||||
decryptSymmetric({
|
|
||||||
ciphertext,
|
|
||||||
iv: modifiedIv,
|
|
||||||
tag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
}).toThrowError('Unsupported state or unable to authenticate data');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should fail if tag is modified', () => {
|
|
||||||
const modifiedTag = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
expect(() => {
|
|
||||||
decryptSymmetric({
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag: modifiedTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
}).toThrowError(/Invalid authentication tag length: \d+/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw an error when decryption fails', () => {
|
|
||||||
const invalidKey = 'invalid-key';
|
|
||||||
expect(() => {
|
|
||||||
decryptSymmetric({
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key: invalidKey
|
|
||||||
});
|
|
||||||
}).toThrowError('Invalid key length');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ func GetKeyRing() (keyring.Keyring, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
||||||
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
|
if password, ok := os.LookupEnv("VAULT_PASS"); ok {
|
||||||
|
return password, nil
|
||||||
|
} else if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
|
||||||
return password, nil
|
return password, nil
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("You may set the environment variable `INFISICAL_VAULT_FILE_PASSPHRASE` with your password to avoid typing it")
|
fmt.Println("To avoid repeatedly typing your password, set the environment variable `VAULT_PASS` to your password")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "%s:", prompt)
|
fmt.Fprintf(os.Stderr, "%s:", prompt)
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ infisical scan install --pre-commit-hook
|
|||||||
To disable this hook after installing it, run the command `git config --bool hooks.infisical-scan false`
|
To disable this hook after installing it, run the command `git config --bool hooks.infisical-scan false`
|
||||||
|
|
||||||
### Third party hooks management
|
### Third party hooks management
|
||||||
If you prefer to manage your pre-commit hook outside of the .git/hooks directory, you can easily accomplish this by adding the following command to your pre-commit script
|
If you would rather handle your pre-commit hook outside of the standard `.git/hooks` directory, you can quickly achieve this by adding the following command into your pre-commit script.
|
||||||
|
For instance, if you utilize [Husky](https://typicode.github.io/husky/) for managing your Git hooks, you can insert the command provided below into your `.husky/pre-commit` file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
infisical scan git-changes --staged --verbose
|
infisical scan git-changes --staged --verbose
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ By deploying Infisical on Kubernetes, you can take advantage of its features to
|
|||||||
To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes.
|
To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes.
|
||||||
|
|
||||||
Helm is a package manager for Kubernetes that simplifies the installation and management of Kubernetes applications.
|
Helm is a package manager for Kubernetes that simplifies the installation and management of Kubernetes applications.
|
||||||
With our Helm chart, you can easily install Infisical on Kubernetes, configure it to your liking, and scale it up or down as needed.
|
With our Helm chart, you can easily install Infisical on Kubernetes, configure it to your liking, and scale it up or down as needed.
|
||||||
|
|
||||||
In the following guide, we'll walk you through the step-by-step process of installing Infisical on Kubernetes using the Helm chart. By the end of this guide, you'll have a fully functional deployment of Infisical running on Kubernetes.
|
|
||||||
|
|
||||||
## Install Infisical Helm repository
|
## Install Infisical Helm repository
|
||||||
|
|
||||||
@@ -34,7 +32,7 @@ Create a values.yaml file to configure various installation settings, such as th
|
|||||||
|
|
||||||
By default, the application will use the latest tag to retrieve the required Docker images, which may be appropriate for most cases.
|
By default, the application will use the latest tag to retrieve the required Docker images, which may be appropriate for most cases.
|
||||||
However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment.
|
However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment.
|
||||||
View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical).
|
View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters).
|
||||||
|
|
||||||
|
|
||||||
To determine the appropriate versions to use for the docker images, follow the links bellow
|
To determine the appropriate versions to use for the docker images, follow the links bellow
|
||||||
@@ -64,7 +62,11 @@ You can configure environment variables for the frontend and backend in your Hel
|
|||||||
|
|
||||||
Infisical requires the following backend environment variables to be defined: _`ENCRYPTION_KEY`_, _`JWT_SIGNUP_SECRET`_, _`JWT_REFRESH_SECRET`_, _`JWT_AUTH_SECRET`_, _`JWT_MFA_SECRET`_ and _`JWT_SERVICE_SECRET`_.
|
Infisical requires the following backend environment variables to be defined: _`ENCRYPTION_KEY`_, _`JWT_SIGNUP_SECRET`_, _`JWT_REFRESH_SECRET`_, _`JWT_AUTH_SECRET`_, _`JWT_MFA_SECRET`_ and _`JWT_SERVICE_SECRET`_.
|
||||||
|
|
||||||
However, when the above environment variables are not defined, our Helm chart
|
<Info>
|
||||||
|
Each of the above environment variables can be generated by running the command `openssl rand -hex 16` in your terminal.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
However, when the above environment variables are not defined, the Helm chart
|
||||||
will automatically generate these environment variables for you. The generated environment variables will be saved to a Kubernetes secret and will be preserved between upgrades or uninstalls.
|
will automatically generate these environment variables for you. The generated environment variables will be saved to a Kubernetes secret and will be preserved between upgrades or uninstalls.
|
||||||
|
|
||||||
```yaml simple-values-example.yaml
|
```yaml simple-values-example.yaml
|
||||||
@@ -101,7 +103,7 @@ mongodb:
|
|||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
To increase data redundancy, we recommend that you use a managed MongoDB service such as AWS Document DB, MongoDB or similar services instead.
|
To increase data redundancy, we recommend that you use a managed document database service such as AWS Document DB, MongoDB or similar services instead.
|
||||||
Managed database connection string can be set in the `backendEnvironmentVariables`.
|
Managed database connection string can be set in the `backendEnvironmentVariables`.
|
||||||
|
|
||||||
#### Example helm values
|
#### Example helm values
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ Please ensure you have Docker and Docker Compose installed for your OS.
|
|||||||
|
|
||||||
- `CD` into the repo
|
- `CD` into the repo
|
||||||
- run command `docker-compose -f docker-compose.dev.yml up --build --force-recreate`
|
- run command `docker-compose -f docker-compose.dev.yml up --build --force-recreate`
|
||||||
- Visit localhost:3000 and the website should be live
|
- Visit localhost:8080 and the website should be live
|
||||||
|
|
||||||
### Steps to shutdown this Docker compose
|
### Steps to shutdown this Docker compose
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const attemptLogin = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
telemetry.identify(email);
|
telemetry.identify(email, email);
|
||||||
telemetry.capture('User Logged In');
|
telemetry.capture('User Logged In');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ class Capturer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identify(id: string) {
|
identify(id: string, email?: string) {
|
||||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||||
try {
|
try {
|
||||||
this.api.identify(id);
|
this.api.identify(id, {
|
||||||
|
email: email
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PostHog', error);
|
console.error('PostHog', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type CardTitleProps = {
|
|||||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600',
|
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600 break-words',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const DeleteActionModal = ({
|
|||||||
<form>
|
<form>
|
||||||
<FormControl
|
<FormControl
|
||||||
label={
|
label={
|
||||||
<div className="pb-2 text-sm">
|
<div className="pb-2 text-sm break-words">
|
||||||
Type <span className="font-bold">{deleteKey}</span> to delete the resource
|
Type <span className="font-bold">{deleteKey}</span> to delete the resource
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { forwardRef, ReactNode } from 'react';
|
import { forwardRef, ReactNode } from 'react';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faCaretDown, faCheck, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
import { faCaretDown, faCaretUp,faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
@@ -63,7 +63,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
style={{ width: 'var(--radix-select-trigger-width)' }}
|
style={{ width: 'var(--radix-select-trigger-width)' }}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ScrollUpButton>
|
<SelectPrimitive.ScrollUpButton>
|
||||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
<div className="flex items-center justify-center">
|
||||||
|
<FontAwesomeIcon icon={faCaretUp} size="sm" />
|
||||||
|
</div>
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
<SelectPrimitive.Viewport className="p-1">
|
<SelectPrimitive.Viewport className="p-1">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -76,7 +78,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
)}
|
)}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
<SelectPrimitive.ScrollDownButton>
|
<SelectPrimitive.ScrollDownButton>
|
||||||
<FontAwesomeIcon icon={faCaretDown} size="xs" />
|
<div className="flex items-center justify-center">
|
||||||
|
<FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||||
|
</div>
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
|
|||||||
@@ -261,9 +261,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
router.push(`/dashboard/${value}`);
|
router.push(`/dashboard/${value}`);
|
||||||
}}
|
}}
|
||||||
position="popper"
|
position="popper"
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
|
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
||||||
>
|
>
|
||||||
{workspaces
|
<div className='h-full no-scrollbar no-scrollbar::-webkit-scrollbar'>
|
||||||
|
{workspaces
|
||||||
.filter((ws) => ws.organization === currentOrg?._id)
|
.filter((ws) => ws.organization === currentOrg?._id)
|
||||||
.map(({ _id, name }) => (
|
.map(({ _id, name }) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -274,7 +275,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
{name}
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
{/* <hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> */}
|
</div>
|
||||||
|
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||||
|
|||||||
Reference in New Issue
Block a user