Merge branch 'main' into snyk-upgrade-3f3d5368cc3b2bbb1bc7ecf70c71c625

This commit is contained in:
Maidul Islam
2023-04-15 09:55:55 -07:00
committed by GitHub
322 changed files with 13604 additions and 2289 deletions
+5 -5
View File
@@ -31,12 +31,12 @@ MONGO_PASSWORD=example
SITE_URL=http://localhost:8080 SITE_URL=http://localhost:8080
# Mail/SMTP # Mail/SMTP
SMTP_HOST= # required SMTP_HOST=
SMTP_USERNAME= # required SMTP_USERNAME=
SMTP_PASSWORD= # required SMTP_PASSWORD=
SMTP_PORT=587 SMTP_PORT=587
SMTP_SECURE=false SMTP_SECURE=false
SMTP_FROM_ADDRESS= # required SMTP_FROM_ADDRESS=
SMTP_FROM_NAME=Infisical SMTP_FROM_NAME=Infisical
# Integration # Integration
@@ -66,4 +66,4 @@ STRIPE_WEBHOOK_SECRET=
STRIPE_PRODUCT_STARTER= STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO= STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
+2 -2
View File
@@ -1,6 +1,6 @@
# Description 📣 # Description 📣
*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.* <!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
## Type ✨ ## Type ✨
@@ -11,7 +11,7 @@
# Tests 🛠️ # Tests 🛠️
*Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible* <!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible -->
```sh ```sh
# Here's some code block to paste some code snippets # Here's some code block to paste some code snippets
+12 -6
View File
@@ -9,6 +9,12 @@ jobs:
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
@@ -45,8 +51,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true push: true
context: backend context: backend
tags: infisical/backend:${{ steps.commit.outputs.short }}, tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest infisical/backend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
@@ -94,8 +100,8 @@ jobs:
push: true push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend context: frontend
tags: infisical/frontend:${{ steps.commit.outputs.short }}, tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest infisical/frontend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -122,7 +128,7 @@ jobs:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials - name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
- name: switch to gamma namespace - name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma run: kubectl config set-context --current --namespace=gamma
- name: test kubectl - name: test kubectl
run: kubectl get ingress run: kubectl get ingress
@@ -135,4 +141,4 @@ jobs:
exit 1 exit 1
else else
echo "Helm upgrade was successful" echo "Helm upgrade was successful"
fi fi
+2 -2
View File
File diff suppressed because one or more lines are too long
+1579 -447
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -1,14 +1,15 @@
{ {
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.281.0", "@aws-sdk/client-secrets-manager": "^3.294.0",
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/tracing": "^7.41.0", "@sentry/tracing": "^7.41.0",
"@sentry/node": "^7.40.0", "@sentry/node": "^7.40.0",
"@sentry/node": "^7.41.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
"aws-sdk": "^2.1324.0", "aws-sdk": "^2.1338.0",
"axios": "^1.1.3", "axios": "^1.1.3",
"axios-retry": "^3.4.0", "axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
@@ -29,9 +30,9 @@
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^6.10.1", "mongoose": "^6.10.3",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.5.4", "posthog-node": "^2.6.0",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@@ -57,7 +58,7 @@
"lint-and-fix": "eslint . --ext .ts --fix", "lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles", "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json", "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down" "posttest": "docker compose -f test-resources/docker-compose.test.yml down"
}, },
@@ -80,7 +81,7 @@
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.2.4", "@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
+15 -2
View File
@@ -13,7 +13,7 @@ export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m'; export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!; export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
export const getMongoURL = () => infisical.get('MONGO_URL')!; export const getMongoURL = () => infisical.get('MONGO_URL')!;
export const getNodeEnv = () => infisical.get('NODE_ENV')!; export const getNodeEnv = () => infisical.get('NODE_ENV')! || 'production';
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true; export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
export const getLokiHost = () => infisical.get('LOKI_HOST')!; export const getLokiHost = () => infisical.get('LOKI_HOST')!;
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!; export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
@@ -48,4 +48,17 @@ export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!; export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true; export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!; export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
export const getHttpsEnabled = () => {
if (getNodeEnv() != "production") {
// no https for anything other than prod
return false
}
if (infisical.get('HTTPS_ENABLED') == undefined || infisical.get('HTTPS_ENABLED') == "") {
// default when no value present
return true
}
return infisical.get('HTTPS_ENABLED') === 'true' && true
}
+7 -7
View File
@@ -15,10 +15,10 @@ import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services'; import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import { import {
getNodeEnv,
getJwtRefreshSecret, getJwtRefreshSecret,
getJwtAuthLifetime, getJwtAuthLifetime,
getJwtAuthSecret getJwtAuthSecret,
getHttpsEnabled
} from '../../config'; } from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
@@ -126,21 +126,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: getHttpsEnabled()
}); });
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction && await EELogService.createLog({
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.ip
}); });
// return (access) token in response // return (access) token in response
return res.status(200).send({ return res.status(200).send({
token: tokens.token, token: tokens.token,
@@ -182,14 +182,14 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: getHttpsEnabled() as boolean
}); });
const logoutAction = await EELogService.createAction({ const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT, name: ACTION_LOGOUT,
userId: req.user._id userId: req.user._id
}); });
logoutAction && await EELogService.createLog({ logoutAction && await EELogService.createLog({
userId: req.user._id, userId: req.user._id,
actions: [logoutAction], actions: [logoutAction],
@@ -12,6 +12,11 @@ import {
getTeams, getTeams,
revokeAccess revokeAccess
} from '../../integrations'; } from '../../integrations';
import {
INTEGRATION_VERCEL_API_URL,
INTEGRATION_RAILWAY_API_URL
} from '../../variables';
import request from '../../config/request';
/*** /***
* Return integration authorization with id [integrationAuthId] * Return integration authorization with id [integrationAuthId]
@@ -188,22 +193,203 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getIntegrationAuthTeams = async (req: Request, res: Response) => { export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
let teams; const teams = await getTeams({
try { integrationAuth: req.integrationAuth,
teams = await getTeams({ accessToken: req.accessToken
integrationAuth: req.integrationAuth, });
accessToken: req.accessToken
return res.status(200).send({
teams
});
}
/**
* Return list of available Vercel (preview) branches for Vercel project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface VercelBranch {
ref: string;
lastCommit: string;
isProtected: boolean;
}
const params = new URLSearchParams({
projectId: appId,
...(req.integrationAuth.teamId ? {
teamId: req.integrationAuth.teamId
} : {})
});
let branches: string[] = [];
if (appId && appId !== '') {
const { data }: { data: VercelBranch[] } = await request.get(
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
branches = data.map((b) => b.ref);
}
return res.status(200).send({
branches
});
}
/**
* Return list of Railway environments for Railway project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayEnvironment {
node: {
id: string;
name: string;
isEphemeral: boolean;
}
}
interface Environment {
environmentId: string;
name: string;
}
let environments: Environment[] = [];
if (appId && appId !== '') {
const query = `
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
edges {
node {
id
name
isEphemeral
}
}
}
}
`;
const variables = {
projectId: appId
}
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email }); environments = edges.map((e: RailwayEnvironment) => {
Sentry.captureException(err); return ({
return res.status(400).send({ name: e.node.name,
message: "Failed to get integration authorization teams" environmentId: e.node.id
});
}); });
} }
return res.status(200).send({ return res.status(200).send({
teams environments
});
}
/**
* Return list of Railway services for Railway project with id
* [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayService {
node: {
id: string;
name: string;
}
}
interface Service {
name: string;
serviceId: string;
}
let services: Service[] = [];
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
}
}
`;
if (appId && appId !== '') {
const variables = {
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
services = edges.map((e: RailwayService) => ({
name: e.node.name,
serviceId: e.node.id
}));
}
return res.status(200).send({
services
}); });
} }
@@ -24,6 +24,9 @@ export const createIntegration = async (req: Request, res: Response) => {
isActive, isActive,
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner, owner,
path, path,
region region
@@ -39,12 +42,15 @@ export const createIntegration = async (req: Request, res: Response) => {
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner, owner,
path, path,
region, region,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId) integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
@@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body; let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -50,6 +50,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
apiKeyData = await new APIKeyData({ apiKeyData = await new APIKeyData({
name, name,
lastUsed: new Date(),
expiresAt, expiresAt,
user: req.user._id, user: req.user._id,
secretHash secretHash
+4 -4
View File
@@ -17,9 +17,9 @@ import {
} from '../../variables'; } from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import { import {
getNodeEnv,
getJwtMfaLifetime, getJwtMfaLifetime,
getJwtMfaSecret getJwtMfaSecret,
getHttpsEnabled
} from '../../config'; } from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
@@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: getHttpsEnabled()
}); });
// case: user does not have MFA enablgged // case: user does not have MFA enablgged
@@ -302,7 +302,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: getHttpsEnabled()
}); });
interface VerifyMfaTokenRes { interface VerifyMfaTokenRes {
@@ -11,7 +11,7 @@ import {
import { SecretVersion } from '../../ee/models'; import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import _ from 'lodash'; import _ from 'lodash';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
/** /**
* Create new workspace environment named [environmentName] under workspace with id * Create new workspace environment named [environmentName] under workspace with id
@@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
throw BadRequestError() throw BadRequestError()
} }
relatedWorkspace.environments.forEach(environment => { relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
if (isReadBlocked && isWriteBlocked) { if (isReadBlocked && isWriteBlocked) {
return return
} else { } else {
+2
View File
@@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController'; import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretsController from './secretsController'; import * as secretsController from './secretsController';
import * as serviceAccountsController from './serviceAccountsController';
import * as environmentController from './environmentController'; import * as environmentController from './environmentController';
import * as tagController from './tagController'; import * as tagController from './tagController';
@@ -20,6 +21,7 @@ export {
apiKeyDataController, apiKeyDataController,
secretController, secretController,
secretsController, secretsController,
serviceAccountsController,
environmentController, environmentController,
tagController tagController
} }
@@ -1,9 +1,11 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { import {
MembershipOrg, MembershipOrg,
Membership, Membership,
Workspace Workspace,
ServiceAccount
} from '../../models'; } from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg'; import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
@@ -260,37 +262,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
} }
} }
*/ */
let workspaces; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId organization: organizationId
}, },
'_id' '_id'
) )
).map((w) => w._id.toString()) ).map((w) => w._id.toString())
); );
workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id user: req.user._id
}).populate('workspace') }).populate('workspace')
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email }); return res.status(200).send({
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
workspaces workspaces
}); });
}
/**
* Return service accounts for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId)
});
return res.status(200).send({
serviceAccounts
});
} }
@@ -7,7 +7,7 @@ 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 { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
/** /**
* Create secret for workspace with id [workspaceId] and environment [environment] * Create secret for workspace with id [workspaceId] and environment [environment]
@@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services';
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret; const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = { const sanitizedSecret: SanitizedSecretForCreate = {
@@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds const secretIdsToDelete: string[] = req.body.secretIds
@@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecret = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id) await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) { if (postHogClient) {
@@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecrets = async (req: Request, res: Response) => { export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@@ -281,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecret = async (req: Request, res: Response) => { export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { environment } = req.query; const { environment } = req.query;
const { workspaceId } = req.params; const { workspaceId } = req.params;
+137 -128
View File
@@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services'; import { EESecretService, EELogService } from '../../ee/services';
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog'; import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { PERMISSION_WRITE_SECRETS } from '../../variables';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag'; import Tag from '../../models/tag';
import _ from 'lodash'; import _, { eq } from 'lodash';
import { import {
BatchSecretRequest, BatchSecretRequest,
BatchSecret BatchSecret
@@ -28,12 +28,13 @@ import {
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
* (used by dashboard)
* @param req * @param req
* @param res * @param res
*/ */
export const batchSecrets = async (req: Request, res: Response) => { export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']); const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { const {
workspaceId, workspaceId,
@@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id) secretIds: createdSecrets.map((n) => n._id)
}) as IAction; }) as IAction;
@@ -328,14 +331,15 @@ export const createSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE) if (req.user) {
if (!hasAccess) { const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS)
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
} }
let listOfSecretsToCreate; let listOfSecretsToCreate;
@@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => {
version: 1, version: 1,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
type, type,
user: type === SECRET_PERSONAL ? req.user : undefined, user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment, environment,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
tags tags
}); });
}) });
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject()); const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
@@ -447,14 +451,18 @@ export const createSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id) secretIds: newlyCreatedSecrets.map((n) => n._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
addAction && await EELogService.createLog({ addAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction], actions: [addAction],
channel, channel,
@@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId workspaceId
}); });
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets added', event: 'secrets added',
distinctId: req.user.email, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: listOfSecretsToCreate.length, numberOfSecrets: listOfSecretsToCreate.length,
environment, environment,
@@ -533,91 +546,120 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const postHogClient = getPostHogClient(); const { tagSlugs } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const { workspaceId, environment, tagSlugs } = req.query; // secrets to return
let secrets: ISecret[] = [];
// query tags table to get all tags ids for the tag names for the given workspace
let tagIds = [];
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped to env and project
let hasWriteOnlyAccess
if (!req.serviceTokenData) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
let secrets: any
let secretQuery: any
if (tagNamesList != undefined && tagNamesList.length != 0) { if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId }) const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName) => {
const tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName }); const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null; return tag ? tag.id : null;
}); });
}
secretQuery = { if (req.user) {
workspace: workspaceId, // case: client authorization is via JWT
environment, const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
$or: [ const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
{ user: userId }, if (hasNoAccess) {
{ user: { $exists: false } } throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
} }
} else {
secretQuery = { const secretQuery: any = {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
$or: [ $or: [
{ user: userId }, { user: req.user._id }, // personal secrets for this user
{ user: { $exists: false } } { user: { $exists: false } } // shared secrets from workspace
], ]
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } }
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
if (hasWriteOnlyAccess) {
// only return the secret keys and not the values since user does not have right to see values
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
} }
} }
if (hasWriteOnlyAccess) { // case: client authorization is via service token
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag") if (req.serviceTokenData) {
} else { const userId = req.serviceTokenData.user._id
secrets = await Secret.find(secretQuery).populate("tags")
const secretQuery: any = {
workspace: workspaceId,
environment,
$or: [
{ user: userId }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
// TODO check if service token has write only permission
secrets = await Secret.find(secretQuery).populate("tags");
}
// case: client authorization is via service account
if (req.serviceAccount) {
const secretQuery: any = {
workspace: workspaceId,
environment,
user: { $exists: false } // shared secrets only from workspace
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
secrets = await Secret.find(secretQuery).populate("tags");
} }
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createAction({ const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
userId: new Types.ObjectId(userId), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id) secretIds: secrets.map((n: any) => n._id)
}); });
readAction && await EELogService.createLog({ readAction && await EELogService.createLog({
userId: new Types.ObjectId(userId), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress: req.ip ipAddress: req.ip
}); });
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pulled', event: 'secrets pulled',
distinctId: userEmail, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
@@ -633,59 +675,6 @@ export const getSecrets = async (req: Request, res: Response) => {
}); });
} }
export const getOnlySecretKeys = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query;
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped
if (!req.serviceTokenData) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const [err, secretKeys] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
)
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
.then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
// readAction && await EELogService.createLog({
// userId: new Types.ObjectId(userId),
// workspaceId: new Types.ObjectId(workspaceId as string),
// actions: [readAction],
// channel,
// ipAddress: req.ip
// });
return res.status(200).send({
secretKeys
});
}
/** /**
* Update secret(s) * Update secret(s)
* @param req * @param req
@@ -736,10 +725,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret { interface PatchSecret {
id: string; id: string;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -865,14 +852,18 @@ export const updateSecrets = async (req: Request, res: Response) => {
const updateAction = await EELogService.createAction({ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
updateAction && await EELogService.createLog({ updateAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [updateAction], actions: [updateAction],
channel, channel,
@@ -884,10 +875,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: key workspaceId: key
}) })
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets modified', event: 'secrets modified',
distinctId: req.user.email, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
@@ -909,7 +905,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
/** /**
* Delete secret(s) with id [workspaceId] and environment [environment] * Delete secret(s)
* @param req * @param req
* @param res * @param res
*/ */
@@ -958,7 +954,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
return res.status(200).send({
message: 'delete secrets!!'
});
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id); const toDelete = req.secrets.map((s: any) => s._id);
@@ -992,14 +992,18 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}); });
const deleteAction = await EELogService.createAction({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
deleteAction && await EELogService.createLog({ deleteAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [deleteAction], actions: [deleteAction],
channel, channel,
@@ -1011,10 +1015,15 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key workspaceId: key
}) })
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets deleted', event: 'secrets deleted',
distinctId: req.user.email, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
@@ -0,0 +1,306 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission
} from '../../models';
import {
CreateServiceAccountDto
} from '../../interfaces/serviceAccounts/dto';
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
import { getSaltRounds } from '../../config';
/**
* Return service account tied to the request (service account) client
* @param req
* @param res
*/
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
return res.status(200).send({
serviceAccount
});
}
/**
* Create a new service account under organization with id [organizationId]
* that has access to workspaces [workspaces]
* @param req
* @param res
* @returns
*/
export const createServiceAccount = async (req: Request, res: Response) => {
const {
name,
organizationId,
publicKey,
expiresIn,
}: CreateServiceAccountDto = req.body;
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const secret = crypto.randomBytes(16).toString('base64');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
// create service account
const serviceAccount = await new ServiceAccount({
name,
organization: new Types.ObjectId(organizationId),
user: req.user,
publicKey,
lastUsed: new Date(),
expiresAt,
secretHash
}).save();
const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.secretHash;
// provision default org-level permission for service account
await new ServiceAccountOrganizationPermission({
serviceAccount: serviceAccount._id
}).save();
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
return res.status(200).send({
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
serviceAccount: serviceAccountObj
});
}
/**
* Change name of service account with id [serviceAccountId] to [name]
* @param req
* @param res
* @returns
*/
export const changeServiceAccountName = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const { name } = req.body;
const serviceAccount = await ServiceAccount.findOneAndUpdate(
{
_id: new Types.ObjectId(serviceAccountId)
},
{
name
},
{
new: true
}
);
return res.status(200).send({
serviceAccount
});
}
/**
* Add a service account key to service account with id [serviceAccountId]
* for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const addServiceAccountKey = async (req: Request, res: Response) => {
const {
workspaceId,
encryptedKey,
nonce
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: req.serviceAccount._d,
workspace: new Types.ObjectId(workspaceId)
}).save();
return serviceAccountKey;
}
/**
* Return workspace-level permission for service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id
}).populate('workspace');
return res.status(200).send({
serviceAccountWorkspacePermissions
});
}
/**
* Add a workspace permission to service account with id [serviceAccountId]
* @param req
* @param res
*/
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const {
environment,
workspaceId,
read = false,
write = false,
encryptedKey,
nonce
} = req.body;
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
return res.status(400).send({
message: 'Failed to validate workspace environment'
});
}
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment
});
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
read,
write
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
});
if (!existingServiceAccountKey) {
await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
}).save();
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete workspace permission from service account with id [serviceAccountId]
* @param req
* @param res
*/
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountWorkspacePermissionId } = req.params;
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId);
if (serviceAccountWorkspacePermission) {
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
const count = await ServiceAccountWorkspacePermission.countDocuments({
serviceAccount,
workspace
});
if (count === 0) {
await ServiceAccountKey.findOneAndDelete({
serviceAccount,
workspace
});
}
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const deleteServiceAccount = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
if (serviceAccount) {
await ServiceAccountKey.deleteMany({
serviceAccount: serviceAccount._id
});
await ServiceAccountOrganizationPermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
await ServiceAccountWorkspacePermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account keys for service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const getServiceAccountKeys = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const serviceAccountKeys = await ServiceAccountKey.find({
serviceAccount: req.serviceAccount._id,
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {})
});
return res.status(200).send({
serviceAccountKeys
});
}
@@ -3,10 +3,16 @@ import { Request, Response } from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
User,
ServiceAccount,
ServiceTokenData ServiceTokenData
} from '../../models'; } from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { ABILITY_READ } from '../../variables/organization'; import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT
} from '../../variables';
import { getSaltRounds } from '../../config'; import { getSaltRounds } from '../../config';
/** /**
@@ -53,59 +59,60 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData; let serviceTokenData;
try { const {
const { name,
name, workspaceId,
workspaceId, environment,
environment, encryptedKey,
encryptedKey, iv,
iv, tag,
tag, expiresIn,
expiresIn, permissions
permissions } = req.body;
} = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ) const secret = crypto.randomBytes(16).toString('hex');
if (!hasAccess) { const secretHash = await bcrypt.hash(secret, getSaltRounds());
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
const secret = crypto.randomBytes(16).toString('hex'); let expiresAt;
const secretHash = await bcrypt.hash(secret, getSaltRounds()); if (!!expiresIn) {
expiresAt = new Date()
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user: req.user._id,
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
} }
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({ return res.status(200).send({
serviceToken, serviceToken,
serviceTokenData serviceTokenData
@@ -119,25 +126,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; const { serviceTokenDataId } = req.params;
try {
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData
}); });
}
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
} }
+23 -23
View File
@@ -8,7 +8,7 @@ import {
import { issueAuthTokens } from '../../helpers/auth'; import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables'; import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request'; import request from '../../config/request';
import { getNodeEnv, getLoopsApiKey } from '../../config'; import { getLoopsApiKey, getHttpsEnabled } from '../../config';
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@@ -24,9 +24,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email, email,
firstName, firstName,
lastName, lastName,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
@@ -38,9 +38,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
protectedKey: string; protectedKey: string;
protectedKeyIV: string; protectedKeyIV: string;
protectedKeyTag: string; protectedKeyTag: string;
publicKey: string; publicKey: string;
encryptedPrivateKey: string; encryptedPrivateKey: string;
encryptedPrivateKeyIV: string; encryptedPrivateKeyIV: string;
@@ -48,11 +48,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
salt: string; salt: string;
verifier: string; verifier: string;
organizationName: string; organizationName: string;
} = req.body; } = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
@@ -66,10 +66,10 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
@@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: getHttpsEnabled()
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -158,9 +158,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
email, email,
firstName, firstName,
lastName, lastName,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
@@ -192,10 +192,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
@@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: getHttpsEnabled()
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -241,7 +241,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
message: 'Failed to complete account setup' message: 'Failed to complete account setup'
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
user, user,
@@ -19,7 +19,7 @@ import {
reformatPullSecrets reformatPullSecrets
} from '../../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services'; import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
interface V2PushSecret { interface V2PushSecret {
@@ -48,7 +48,7 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body; let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -506,5 +506,4 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
message: 'Successfully changed autoCapitalization setting', message: 'Successfully changed autoCapitalization setting',
workspace workspace
}); });
}; };
@@ -2,7 +2,8 @@ import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models"; import { Membership, Workspace } from "../../../models";
import { IMembershipPermission } from "../../../models/membership"; import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization"; import { ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables';
import { Builder } from "builder-pattern" import { Builder } from "builder-pattern"
import _ from "lodash"; import _ from "lodash";
@@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
const { membershipId } = req.params; const { membershipId } = req.params;
const { permissions } = req.body; const { permissions } = req.body;
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => { const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) { if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) {
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" }) throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
} }
@@ -418,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.skip(offset) .skip(offset)
.limit(limit) .limit(limit)
.populate('actions') .populate('actions')
.populate('user'); .populate('user serviceAccount serviceTokenData');
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
+33 -10
View File
@@ -24,11 +24,15 @@ import {
const createActionUpdateSecret = async ({ const createActionUpdateSecret = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
@@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({
action = await new Action({ action = await new Action({
name, name,
user: userId, user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions
@@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({
const createActionSecret = async ({ const createActionSecret = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
@@ -94,6 +104,8 @@ const createActionSecret = async ({
action = await new Action({ action = await new Action({
name, name,
user: userId, user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions
@@ -110,29 +122,36 @@ const createActionSecret = async ({
} }
/** /**
* Create an (audit) action for user with id [userId] * Create an (audit) action for client with id [userId],
* [serviceAccountId], or [serviceTokenDataId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.name - name of action * @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action * @param {String} obj.userId - id of user associated with action
* @returns * @returns
*/ */
const createActionUser = ({ const createActionClient = ({
name, name,
userId userId,
serviceAccountId,
serviceTokenDataId
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
}) => { }) => {
let action; let action;
try { try {
action = new Action({ action = new Action({
name, name,
user: userId user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save(); }).save();
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to create user action'); throw new Error('Failed to create client action');
} }
return action; return action;
@@ -149,11 +168,15 @@ const createActionUser = ({
const createActionHelper = async ({ const createActionHelper = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds, secretIds,
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) => { }) => {
@@ -162,7 +185,7 @@ const createActionHelper = async ({
switch (name) { switch (name) {
case ACTION_LOGIN: case ACTION_LOGIN:
case ACTION_LOGOUT: case ACTION_LOGOUT:
action = await createActionUser({ action = await createActionClient({
name, name,
userId userId
}); });
@@ -1,8 +1,9 @@
import { Types } from 'mongoose';
import _ from "lodash"; import _ from "lodash";
import { Membership } from "../../models"; import { Membership } from "../../models";
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
@@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
return true return true
} }
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
// case: you have write only if read is blocked and write is not // case: you have write only if read is blocked and write is not
if (isReadDisallowed && !isWriteDisallowed) { if (isReadDisallowed && !isWriteDisallowed) {
@@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
return false return false
} }
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return true return true
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
if (isReadBlocked && isWriteDisallowed) { if (isReadBlocked && isWriteDisallowed) {
return true return true
+7 -1
View File
@@ -16,12 +16,16 @@ import {
*/ */
const createLogHelper = async ({ const createLogHelper = async ({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress
}: { }: {
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
@@ -31,6 +35,8 @@ const createLogHelper = async ({
try { try {
log = await new Log({ log = await new Log({
user: userId, user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined, workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name), actionNames: actions.map((a) => a.name),
actions, actions,
@@ -15,32 +15,28 @@ import {
const requireSecretSnapshotAuth = ({ const requireSecretSnapshotAuth = ({
acceptedRoles, acceptedRoles,
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const { secretSnapshotId } = req.params;
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
if (!secretSnapshot) { return next(SecretSnapshotNotFoundError({
return next(SecretSnapshotNotFoundError({ message: 'Failed to find secret snapshot'
message: 'Failed to find secret snapshot' }));
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secretSnapshot.workspace.toString(),
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
} }
await validateMembership({
userId: req.user._id,
workspaceId: secretSnapshot.workspace,
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} }
} }
+11 -2
View File
@@ -11,6 +11,8 @@ import {
export interface IAction { export interface IAction {
name: string; name: string;
user?: Types.ObjectId, user?: Types.ObjectId,
serviceAccount?: Types.ObjectId,
serviceTokenData?: Types.ObjectId,
workspace?: Types.ObjectId, workspace?: Types.ObjectId,
payload?: { payload?: {
secretVersions?: Types.ObjectId[] secretVersions?: Types.ObjectId[]
@@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: 'User'
required: true },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
+10
View File
@@ -11,6 +11,8 @@ import {
export interface ILog { export interface ILog {
_id: Types.ObjectId; _id: Types.ObjectId;
user?: Types.ObjectId; user?: Types.ObjectId;
serviceAccount?: Types.ObjectId;
serviceTokenData?: Types.ObjectId;
workspace?: Types.ObjectId; workspace?: Types.ObjectId;
actionNames: string[]; actionNames: string[];
actions: Types.ObjectId[]; actions: Types.ObjectId[];
@@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User' ref: 'User'
}, },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace' ref: 'Workspace'
+10 -3
View File
@@ -7,7 +7,12 @@ import {
} from '../../../middleware'; } from '../../../middleware';
import { query, param, body } from 'express-validator'; import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../../variables'; import {
ADMIN,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../../variables';
router.get( router.get(
'/:secretId/secret-versions', '/:secretId/secret-versions',
@@ -15,7 +20,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),
@@ -30,7 +36,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
body('version').exists().isInt(), body('version').exists().isInt(),
+8 -4
View File
@@ -15,7 +15,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),
@@ -30,7 +31,8 @@ router.get(
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -43,7 +45,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('version').exists().isInt(), body('version').exists().isInt(),
@@ -57,7 +60,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),
+14 -2
View File
@@ -26,12 +26,16 @@ class EELogService {
*/ */
static async createLog({ static async createLog({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress
}: { }: {
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
@@ -40,6 +44,8 @@ class EELogService {
if (!EELicenseService.isLicenseValid) return null; if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({ return await createLogHelper({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
@@ -59,17 +65,23 @@ class EELogService {
static async createAction({ static async createAction({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) { }) {
return await createActionHelper({ return await createActionHelper({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}); });
+146 -122
View File
@@ -1,15 +1,18 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
IUser, IUser,
User, User,
ServiceTokenData, ServiceTokenData,
ServiceAccount,
APIKeyData APIKeyData
} from '../models'; } from '../models';
import { import {
AccountNotFoundError, AccountNotFoundError,
ServiceTokenDataNotFoundError, ServiceTokenDataNotFoundError,
ServiceAccountNotFoundError,
APIKeyDataNotFoundError, APIKeyDataNotFoundError,
UnauthorizedRequestError, UnauthorizedRequestError,
BadRequestError BadRequestError
@@ -20,6 +23,12 @@ import {
getJwtRefreshLifetime, getJwtRefreshLifetime,
getJwtRefreshSecret getJwtRefreshSecret
} from '../config'; } from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/** /**
* *
@@ -37,7 +46,7 @@ const validateAuthMode = ({
const apiKey = headers['x-api-key']; const apiKey = headers['x-api-key'];
const authHeader = headers['authorization']; const authHeader = headers['authorization'];
let authTokenType, authTokenValue; let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) { if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present // case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' }); throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@@ -45,7 +54,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') { if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key) // case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey'; authMode = AUTH_MODE_API_KEY;
authTokenValue = apiKey; authTokenValue = apiKey;
} }
@@ -61,20 +70,24 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) { switch (tokenValue.split('.', 1)[0]) {
case 'st': case 'st':
authTokenType = 'serviceToken'; authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case 'sa':
authMode = AUTH_MODE_SERVICE_ACCOUNT;
break; break;
default: default:
authTokenType = 'jwt'; authMode = AUTH_MODE_JWT;
} }
authTokenValue = tokenValue; authTokenValue = tokenValue;
} }
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' }); if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' }); if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({ return ({
authTokenType, authMode,
authTokenValue authTokenValue
}); });
} }
@@ -90,25 +103,17 @@ const getAuthUserPayload = async ({
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let user; const decodedToken = <jwt.UserIDJwtPayload>(
try { jwt.verify(authTokenValue, getJwtAuthSecret())
const decodedToken = <jwt.UserIDJwtPayload>( );
jwt.verify(authTokenValue, getJwtAuthSecret())
);
user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: decodedToken.userId
}).select('+publicKey'); }).select('+publicKey');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate JWT token'
});
}
return user; return user;
} }
@@ -124,45 +129,70 @@ const getAuthSTDPayload = async ({
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let serviceTokenData; const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
// TODO: optimize double query let serviceTokenData = await ServiceTokenData
serviceTokenData = await ServiceTokenData .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
if (!serviceTokenData) { if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired // case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired service token'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} catch (err) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: 'Failed to authenticate service token' message: 'Failed to authenticate expired service token'
}); });
} }
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
})
.select('+encryptedKey +iv +tag').populate('user serviceAccount');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
return serviceTokenData; return serviceTokenData;
} }
/**
* Return service account access key payload
* @param {Object} obj
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
const getAuthSAAKPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const serviceAccount = await ServiceAccount.findById(
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
).select('+secretHash');
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
if (!result) throw UnauthorizedRequestError({
message: 'Failed to authenticate service account access key'
});
return serviceAccount;
}
/** /**
* Return API key data payload corresponding to API key [authTokenValue] * Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj * @param {Object} obj
@@ -174,33 +204,44 @@ const getAuthAPIKeyPayload = async ({
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let user; const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const apiKeyData = await APIKeyData let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey'); .populate<{ user: IUser }>('user', '+publicKey');
if (!apiKeyData) { if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired // case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id); await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
user = apiKeyData.user;
} catch (err) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: 'Failed to authenticate API key' message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
});
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
}
const user = await User.findById(apiKeyData.user).select('+publicKey');
if (!user) {
throw AccountNotFoundError({
message: 'Failed to find user'
}); });
} }
@@ -216,30 +257,23 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.refreshToken - issued refresh token * @return {String} obj.refreshToken - issued refresh token
*/ */
const issueAuthTokens = async ({ userId }: { userId: string }) => { const issueAuthTokens = async ({ userId }: { userId: string }) => {
let token: string;
let refreshToken: string;
try {
// issue tokens
token = createToken({
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});
refreshToken = createToken({ // issue tokens
payload: { const token = createToken({
userId payload: {
}, userId
expiresIn: getJwtRefreshLifetime(), },
secret: getJwtRefreshSecret() expiresIn: getJwtAuthLifetime(),
}); secret: getJwtAuthSecret()
} catch (err) { });
Sentry.setUser(null);
Sentry.captureException(err); const refreshToken = createToken({
throw new Error('Failed to issue tokens'); payload: {
} userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
});
return { return {
token, token,
@@ -253,19 +287,14 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {String} obj.userId - id of user whose tokens are cleared. * @param {String} obj.userId - id of user whose tokens are cleared.
*/ */
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => { const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
try { // increment refreshVersion on user by 1
// increment refreshVersion on user by 1 User.findOneAndUpdate({
User.findOneAndUpdate({ _id: userId
_id: userId }, {
}, { $inc: {
$inc: { refreshVersion: 1
refreshVersion: 1 }
} });
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
}; };
/** /**
@@ -285,21 +314,16 @@ const createToken = ({
expiresIn: string | number; expiresIn: string | number;
secret: string; secret: string;
}) => { }) => {
try { return jwt.sign(payload, secret, {
return jwt.sign(payload, secret, { expiresIn
expiresIn });
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create a token');
}
}; };
export { export {
validateAuthMode, validateAuthMode,
getAuthUserPayload, getAuthUserPayload,
getAuthSTDPayload, getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload, getAuthAPIKeyPayload,
createToken, createToken,
issueAuthTokens, issueAuthTokens,
+89 -2
View File
@@ -1,10 +1,16 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { import {
Bot, Bot,
BotKey, BotKey,
Secret, Secret,
ISecret, ISecret,
IUser IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models'; } from '../models';
import { import {
generateKeyPair, generateKeyPair,
@@ -12,8 +18,88 @@ import {
decryptSymmetric, decryptSymmetric,
decryptAsymmetric decryptAsymmetric
} from '../utils/crypto'; } from '../utils/crypto';
import { SECRET_SHARED } from '../variables'; import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { getEncryptionKey } from '../config'; import { getEncryptionKey } from '../config';
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
/**
* 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]
@@ -222,6 +308,7 @@ const decryptSymmetricHelper = async ({
} }
export { export {
validateClientForBot,
createBot, createBot,
getSecretsHelper, getSecretsHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
+108 -4
View File
@@ -1,17 +1,42 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
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
} from '../variables'; } from '../variables';
import { UnauthorizedRequestError } from '../utils/errors'; import {
UnauthorizedRequestError,
IntegrationAuthNotFoundError,
IntegrationNotFoundError
} 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;
@@ -20,6 +45,84 @@ 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]
@@ -140,7 +243,7 @@ const syncIntegrationsHelper = async ({
// get integration auth access token // get integration auth access token
const access = await getIntegrationAuthAccessHelper({ const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString() integrationAuthId: integration.integrationAuth
}); });
// sync secrets to integration // sync secrets to integration
@@ -167,7 +270,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken; let refreshToken;
try { try {
@@ -204,7 +307,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId; let accessId;
let accessToken; let accessToken;
try { try {
@@ -367,6 +470,7 @@ const setIntegrationAuthAccessHelper = async ({
} }
export { export {
validateClientForIntegration,
handleOAuthExchangeHelper, handleOAuthExchangeHelper,
syncIntegrationsHelper, syncIntegrationsHelper,
getIntegrationAuthRefreshHelper, getIntegrationAuthRefreshHelper,
+108
View File
@@ -0,0 +1,108 @@
import { Types } from 'mongoose';
import {
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IWorkspace
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
IntegrationAuthNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
/**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationAuthId - id of integration authorization to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegrationAuth = async ({
authData,
integrationAuthId,
acceptedRoles,
attachAccessToken
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationAuthId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean;
}) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
let accessToken;
if (attachAccessToken) {
accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integrationAuth.workspace._id
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration authorization'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration authorization'
});
}
export {
validateClientForIntegrationAuth
};
+117 -19
View File
@@ -1,5 +1,106 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Membership, Key } from '../models'; import { Types } from 'mongoose';
import {
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipNotFoundError,
BadRequestError,
UnauthorizedRequestError
} 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]
@@ -14,28 +115,24 @@ const validateMembership = async ({
workspaceId, workspaceId,
acceptedRoles, acceptedRoles,
}: { }: {
userId: string; userId: Types.ObjectId;
workspaceId: string; workspaceId: Types.ObjectId;
acceptedRoles: string[]; acceptedRoles?: Array<'admin' | 'member'>;
}) => { }) => {
let membership; const membership = await Membership.findOne({
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors user: userId,
try { workspace: workspaceId
membership = await Membership.findOne({ }).populate("workspace");
user: userId,
workspace: workspaceId if (!membership) {
}).populate("workspace"); throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
}
if (!membership) throw new Error('Failed to find membership');
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) { if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate membership role'); throw BadRequestError({ message: 'Failed authorization for membership role' });
} }
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate membership');
} }
return membership; return membership;
@@ -133,6 +230,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
}; };
export { export {
validateClientForMembership,
validateMembership, validateMembership,
addMemberships, addMemberships,
findMembership, findMembership,
+126 -25
View File
@@ -1,40 +1,140 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { MembershipOrg, Workspace, Membership, Key } from '../models'; import {
MembershipOrg,
Workspace,
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipOrgNotFoundError,
BadRequestError,
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
*/
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]
* and has at least one of the roles in [acceptedRoles] * and has at least one of the roles in [acceptedRoles]
* * @param {Object} obj
* @param {Types.ObjectId} obj.userId
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*/ */
const validateMembership = async ({ const validateMembershipOrg = async ({
userId, userId,
organizationId, organizationId,
acceptedRoles acceptedRoles,
acceptedStatuses
}: { }: {
userId: string; userId: Types.ObjectId;
organizationId: string; organizationId: Types.ObjectId;
acceptedRoles: string[]; acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses?: Array<'invited' | 'accepted'>;
}) => { }) => {
let membership; const membershipOrg = await MembershipOrg.findOne({
try { user: userId,
membership = await MembershipOrg.findOne({ organization: organizationId
user: new Types.ObjectId(userId), });
organization: new Types.ObjectId(organizationId)
}); if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
if (!membership) throw new Error('Failed to find organization membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate organization membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate organization membership');
} }
return membership; if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
}
}
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
}
}
return membershipOrg;
} }
/** /**
@@ -156,7 +256,8 @@ const deleteMembershipOrg = async ({
}; };
export { export {
validateMembership, validateClientForMembershipOrg,
validateMembershipOrg,
findMembershipOrg, findMembershipOrg,
addMembershipsOrg, addMembershipsOrg,
deleteMembershipOrg deleteMembershipOrg
+98 -1
View File
@@ -1,14 +1,110 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { ACCEPTED } from '../variables'; import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import { Organization, MembershipOrg } from '../models'; import { Organization, MembershipOrg } from '../models';
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER
} from '../variables';
import { import {
getStripeSecretKey, getStripeSecretKey,
getStripeProductPro, getStripeProductPro,
getStripeProductTeam, getStripeProductTeam,
getStripeProductStarter getStripeProductStarter
} from '../config'; } from '../config';
import {
UnauthorizedRequestError,
OrganizationNotFoundError
} from '../utils/errors';
import {
validateUserClientForOrganization
} from '../helpers/user';
import {
validateServiceAccountClientForOrganization
} from '../helpers/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
*/
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]
@@ -172,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({
}; };
export { export {
validateClientForOrganization,
createOrganization, createOrganization,
initSubscriptionOrg, initSubscriptionOrg,
updateSubscriptionOrgQuantity updateSubscriptionOrgQuantity
+12 -4
View File
@@ -15,7 +15,7 @@ const apiLimiter = rateLimit({
}); });
// 10 requests per minute // 10 requests per minute
const authLimiter = rateLimit({ const authLimit = rateLimit({
windowMs: 60 * 1000, windowMs: 60 * 1000,
max: 10, max: 10,
standardHeaders: true, standardHeaders: true,
@@ -36,8 +36,16 @@ const passwordLimiter = rateLimit({
} }
}); });
export { const authLimiter = (req: any, res: any, next: any) => {
apiLimiter, if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
authLimiter, authLimiter,
passwordLimiter passwordLimiter
}; };
-53
View File
@@ -21,60 +21,8 @@ import {
ACTION_READ_SECRETS ACTION_READ_SECRETS
} from '../variables'; } from '../variables';
import _ from 'lodash'; import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that user with id [userId] can modify secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.userId - id of user to validate
* @param {Object} obj.secretIds - secret ids
* @returns {Secret[]} secrets
*/
const validateSecrets = async ({
userId,
secretIds
}: {
userId: string;
secretIds: string[];
}) => {
let secrets;
try {
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Unable to validate some secrets' })
}
const userMemberships = await Membership.find({ user: userId })
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())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
}
} else {
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
}
});
} catch (err) {
throw BadRequestError({ message: 'Unable to validate secrets' })
}
return secrets;
}
interface V1PushSecret { interface V1PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
@@ -714,7 +662,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
}; };
export { export {
validateSecrets,
v1PushSecrets, v1PushSecrets,
v2PushSecrets, v2PushSecrets,
pullSecrets, pullSecrets,
+198
View File
@@ -0,0 +1,198 @@
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
Secret,
ISecret
} from '../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 {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError
} 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
*/
const validateClientForSecret = async ({
authData,
secretId,
acceptedRoles,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
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: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
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'
});
}
export {
validateClientForSecret,
validateClientForSecrets
}
+271
View File
@@ -0,0 +1,271 @@
import _ from 'lodash';
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
ISecret,
IOrganization,
IServiceAccountWorkspacePermission,
ServiceAccountWorkspacePermission
} from '../models';
import {
BadRequestError,
UnauthorizedRequestError,
ServiceAccountNotFoundError
} from '../utils/errors';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForServiceAccount
} from '../helpers/user';
const validateClientForServiceAccount = async ({
authData,
serviceAccountId,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
serviceAccountId: Types.ObjectId;
requiredPermissions?: string[];
}) => {
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({
message: 'Failed to find service account'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForServiceAccount({
user: authData.authPayload,
serviceAccount,
requiredPermissions
});
return serviceAccount;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForServiceAccount({
serviceAccount: authData.authPayload,
targetServiceAccount: serviceAccount,
requiredPermissions
});
return serviceAccount;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for service account resource'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForServiceAccount({
user: authData.authPayload,
serviceAccount,
requiredPermissions
});
return serviceAccount;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for service account resource'
});
}
/**
* Validate that service account (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceAccount} obj.serviceAccount - service account 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 validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (environment) {
// case: environment specified ->
// evaluate service account authorization for workspace
// in the context of a specific environment [environment]
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount,
workspace: new Types.ObjectId(workspaceId),
environment
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
if (!permission.read) runningIsDisallowed = true;
break;
case PERMISSION_WRITE_SECRETS:
if (!permission.write) runningIsDisallowed = true;
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
} else {
// case: no environment specified ->
// evaluate service account authorization for workspace
// without need of environment [environment]
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount,
workspace: new Types.ObjectId(workspaceId)
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace'
});
}
}
/**
* Validate that service account (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id
});
const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}`
});
secrets.forEach((secret: ISecret) => {
const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`];
if (!permission) throw BadRequestError({
message: 'Failed to find any permission for the secret workspace and environment'
});
requiredPermissions?.forEach((requiredPermission: string) => {
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
if (!permission.read) runningIsDisallowed = true;
break;
case PERMISSION_WRITE_SECRETS:
if (!permission.write) runningIsDisallowed = true;
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
});
});
}
/**
* Validate that service account (client) can access target service
* account [serviceAccount] with required permissions [requiredPermissions]
* @param {Object} obj
* @param {SerivceAccount} obj.serviceAccount - service account client
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForServiceAccount = ({
serviceAccount,
targetServiceAccount,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
targetServiceAccount: IServiceAccount;
requiredPermissions?: string[];
}) => {
if (!serviceAccount.organization.equals(targetServiceAccount.organization)) {
throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given service account'
});
}
}
/**
* Validate that service account (client) can access organization [organization]
* @param {Object} obj
* @param {User} obj.user - service account client
* @param {Organization} obj.organization - organization to validate against
*/
const validateServiceAccountClientForOrganization = async ({
serviceAccount,
organization
}: {
serviceAccount: IServiceAccount;
organization: IOrganization;
}) => {
if (!serviceAccount.organization.equals(organization._id)) {
throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given organization'
});
}
}
export {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount,
validateServiceAccountClientForOrganization
}
+189
View File
@@ -0,0 +1,189 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData,
ServiceTokenData,
IUser,
User,
IServiceAccount,
ServiceAccount,
} from '../models';
import {
UnauthorizedRequestError,
ServiceTokenDataNotFoundError
} 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';
/**
* Validate authenticated clients for service token with id [serviceTokenId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForServiceTokenData = async ({
authData,
serviceTokenDataId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
serviceTokenDataId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const serviceTokenData = await ServiceTokenData
.findById(serviceTokenDataId)
.select('+encryptedKey +iv +tag')
.populate<{ user: IUser }>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({
message: 'Failed to find service token data'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: serviceTokenData.workspace,
acceptedRoles
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: serviceTokenData.workspace
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for service token data'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: serviceTokenData.workspace,
acceptedRoles
});
return serviceTokenData;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for service token data'
});
}
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token 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 validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (environment) {
// case: environment is specified
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
}
}
/**
* Validate that service token (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
secrets.forEach((secret: ISecret) => {
if (!serviceTokenData.workspace.equals(secret.workspace)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== secret.environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
});
}
export {
validateClientForServiceTokenData,
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}
View File
+221 -2
View File
@@ -1,6 +1,25 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { IUser, User } from '../models'; import { Types } from 'mongoose';
import {
IUser,
ISecret,
IServiceAccount,
User,
Membership,
IOrganization,
Organization,
} 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]
@@ -146,4 +165,204 @@ const checkUserDevice = async ({
} }
} }
export { setupAccount, completeAccount, checkUserDevice }; /**
* 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
};
+109 -2
View File
@@ -1,12 +1,115 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { import {
Workspace, Workspace,
Bot, Bot,
Membership, Membership,
Key, Key,
Secret Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
} 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';
/**
* 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
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
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 });
}
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]
@@ -71,4 +174,8 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
} }
}; };
export { createWorkspace, deleteWorkspace }; export {
validateClientForWorkspace,
createWorkspace,
deleteWorkspace
};
+5 -3
View File
@@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node';
import { DatabaseService } from './services'; import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health'; import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp'; import { initSmtp } from './services/smtp';
import { logTelemetryMessage } from './services'; import { TelemetryService } from './services';
import { setTransporter } from './helpers/nodemailer'; import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser'; import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -56,6 +56,7 @@ import {
secret as v2SecretRouter, // begin to phase out secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter, secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter, apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter, environment as v2EnvironmentRouter,
tags as v2TagsRouter, tags as v2TagsRouter,
@@ -79,7 +80,7 @@ const main = async () => {
}); });
} }
logTelemetryMessage(); TelemetryService.logTelemetryMessage();
setTransporter(initSmtp()); setTransporter(initSmtp());
await DatabaseService.initDatabase(getMongoURL()); await DatabaseService.initDatabase(getMongoURL());
@@ -150,6 +151,7 @@ const main = async () => {
app.use('/api/v2/secret', v2SecretRouter); // deprecated app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter); app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs // api docs
@@ -170,7 +172,7 @@ const main = async () => {
getLogger("backend-main").info(`Server started listening at port ${getPort()}`) getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
}); });
createTestUserForDevelopment(); await createTestUserForDevelopment();
setUpHealthEndpoint(server); setUpHealthEndpoint(server);
server.on('close', async () => { server.on('close', async () => {
+110 -4
View File
@@ -12,17 +12,21 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL, INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables"; } from "../variables";
interface App { interface App {
@@ -94,6 +98,11 @@ const getApps = async ({
accessToken, accessToken,
}); });
break; break;
case INTEGRATION_RAILWAY:
apps = await getAppsRailway({
accessToken
});
break;
case INTEGRATION_FLYIO: case INTEGRATION_FLYIO:
apps = await getAppsFlyio({ apps = await getAppsFlyio({
accessToken, accessToken,
@@ -109,6 +118,11 @@ const getApps = async ({
accessToken, accessToken,
}) })
break; break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken
});
break;
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -184,6 +198,7 @@ const getAppsVercel = async ({
apps = res.projects.map((a: any) => ({ apps = res.projects.map((a: any) => ({
name: a.name, name: a.name,
appId: a.id
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -270,10 +285,13 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
apps = repos apps = repos
.filter((a: any) => a.permissions.admin === true) .filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({ .map((a: any) => {
name: a.name, return ({
owner: a.owner.login, appId: a.id,
})); name: a.name,
owner: a.owner.login,
});
});
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -319,6 +337,58 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
return apps; return apps;
}; };
/**
* Return list of projects for Railway integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Railway API
* @returns {Object[]} apps - names and ids of Railway services
* @returns {String} apps.name - name of Railway project
* @returns {String} apps.appId - id of Railway project
*
*/
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
let apps: any[] = [];
try {
const query = `
query GetProjects($userId: String, $teamId: String) {
projects(userId: $userId, teamId: $teamId) {
edges {
node {
id
name
}
}
}
}
`;
const variables = {};
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});
apps = edges.map((e: any) => ({
name: e.node.name,
appId: e.node.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Railway services");
}
return apps;
}
/** /**
* Return list of apps for Fly.io integration * Return list of apps for Fly.io integration
* @param {Object} obj * @param {Object} obj
@@ -545,4 +615,40 @@ const getAppsGitlab = async ({
return apps; return apps;
} }
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const { data } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
apps = data.map((a: any) => {
return {
name: a.name,
appId: a.id
};
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Supabase projects');
}
return apps;
};
export { getApps }; export { getApps };
+175 -44
View File
@@ -21,19 +21,24 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL, INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables"; } from "../variables";
import request from '../config/request'; import request from '../config/request';
import axios from "axios";
/** /**
* Sync/push [secrets] to [app] in integration named [integration] * Sync/push [secrets] to [app] in integration named [integration]
@@ -126,6 +131,13 @@ const syncSecrets = async ({
accessToken, accessToken,
}); });
break; break;
case INTEGRATION_RAILWAY:
await syncSecretsRailway({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_FLYIO: case INTEGRATION_FLYIO:
await syncSecretsFlyio({ await syncSecretsFlyio({
integration, integration,
@@ -147,6 +159,13 @@ const syncSecrets = async ({
accessToken, accessToken,
}); });
break; break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken
});
break;
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -608,6 +627,7 @@ const syncSecretsVercel = async ({
key: string; key: string;
value: string; value: string;
target: string[]; target: string[];
gitBranch?: string;
} }
try { try {
@@ -621,46 +641,7 @@ const syncSecretsVercel = async ({
} }
: {}), : {}),
}; };
// const res = (
// await Promise.all(
// (
// await request.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// ))
// .data
// .envs
// .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
// .map(async (secret: VercelSecret) => {
// if (secret.type === 'encrypted') {
// // case: secret is encrypted -> need to decrypt
// const decryptedSecret = (await request.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// )).data;
// return decryptedSecret;
// }
// return secret;
// }))).reduce((obj: any, secret: any) => ({
// ...obj,
// [secret.key]: secret
// }), {});
const vercelSecrets: VercelSecret[] = (await request.get( const vercelSecrets: VercelSecret[] = (await request.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{ {
@@ -673,7 +654,21 @@ const syncSecretsVercel = async ({
)) ))
.data .data
.envs .envs
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)); .filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment
return false;
}
if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch
return false;
}
return true;
});
// return secret.target.includes(integration.targetEnvironment);
const res: { [key: string]: VercelSecret } = {}; const res: { [key: string]: VercelSecret } = {};
@@ -696,7 +691,7 @@ const syncSecretsVercel = async ({
res[vercelSecret.key] = vercelSecret; res[vercelSecret.key] = vercelSecret;
} }
} }
const updateSecrets: VercelSecret[] = []; const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = [];
@@ -710,6 +705,9 @@ const syncSecretsVercel = async ({
value: secrets[key], value: secrets[key],
type: "encrypted", type: "encrypted",
target: [integration.targetEnvironment], target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
}); });
} }
}); });
@@ -726,7 +724,10 @@ const syncSecretsVercel = async ({
type: res[key].type, type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment) target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target] ? [...res[key].target]
: [...res[key].target, integration.targetEnvironment] : [...res[key].target, integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
}); });
} }
} else { } else {
@@ -737,6 +738,9 @@ const syncSecretsVercel = async ({
value: res[key].value, value: res[key].value,
type: "encrypted", // value doesn't matter type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment], target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
}); });
} }
}); });
@@ -1060,7 +1064,7 @@ const syncSecretsGitHub = async ({
"GET /repos/{owner}/{repo}/actions/secrets/public-key", "GET /repos/{owner}/{repo}/actions/secrets/public-key",
{ {
owner: integration.owner, owner: integration.owner,
repo: integration.app, repo: integration.app
} }
) )
).data; ).data;
@@ -1167,6 +1171,58 @@ const syncSecretsRender = async ({
} }
}; };
/**
* Sync/push [secrets] to Railway project with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Railway integration
*/
const syncSecretsRailway = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
}
`;
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: secrets
};
await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables: {
input,
},
}, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Railway");
}
}
/** /**
* Sync/push [secrets] to Fly.io app * Sync/push [secrets] to Fly.io app
* @param {Object} obj * @param {Object} obj
@@ -1571,4 +1627,79 @@ const syncSecretsGitLab = async ({
} }
} }
/**
* Sync/push [secrets] to Supabase with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Supabase integration
*/
const syncSecretsSupabase = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const { data: getSecretsRes } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
// convert the secrets to [{}] format
const modifiedFormatForSecretInjection = Object.keys(secrets).map(
(key) => {
return {
name: key,
value: secrets[key]
};
}
);
await request.post(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
const secretsToDelete: any = [];
getSecretsRes?.forEach((secretObj: any) => {
if (!(secretObj.name in secrets)) {
secretsToDelete.push(secretObj.name);
}
});
await request.delete(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
data: secretsToDelete
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Supabase');
}
};
export { syncSecrets }; export { syncSecrets };
@@ -0,0 +1,7 @@
interface AddServiceAccountPermissionDto {
name: string;
workspaceId?: string;
environment?: string;
}
export default AddServiceAccountPermissionDto;
@@ -0,0 +1,8 @@
interface CreateServiceAccountDto {
organizationId: string;
name: string;
publicKey: string;
expiresIn: number;
}
export default CreateServiceAccountDto;
@@ -0,0 +1,7 @@
import CreateServiceAccountDto from './CreateServiceAccountDto';
import AddServiceAccountPermissionDto from './AddServiceAccountPermissionDto';
export {
CreateServiceAccountDto,
AddServiceAccountPermissionDto
}
+4
View File
@@ -10,6 +10,8 @@ import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
import requireServiceTokenAuth from './requireServiceTokenAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth'; import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import requireServiceAccountAuth from './requireServiceAccountAuth';
import requireServiceAccountWorkspacePermissionAuth from './requireServiceAccountWorkspacePermissionAuth';
import requireSecretAuth from './requireSecretAuth'; import requireSecretAuth from './requireSecretAuth';
import requireSecretsAuth from './requireSecretsAuth'; import requireSecretsAuth from './requireSecretsAuth';
import validateRequest from './validateRequest'; import validateRequest from './validateRequest';
@@ -27,6 +29,8 @@ export {
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
requireServiceTokenDataAuth, requireServiceTokenDataAuth,
requireServiceAccountAuth,
requireServiceAccountWorkspacePermissionAuth,
requireSecretAuth, requireSecretAuth,
requireSecretsAuth, requireSecretsAuth,
validateRequest validateRequest
+44 -24
View File
@@ -4,11 +4,23 @@ import {
validateAuthMode, validateAuthMode,
getAuthUserPayload, getAuthUserPayload,
getAuthSTDPayload, getAuthSTDPayload,
getAuthAPIKeyPayload getAuthAPIKeyPayload,
getAuthSAAKPayload
} from '../helpers/auth'; } from '../helpers/auth';
import { import {
UnauthorizedRequestError UnauthorizedRequestError
} from '../utils/errors'; } from '../utils/errors';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -27,50 +39,58 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
const requireAuth = ({ const requireAuth = ({
acceptedAuthModes = ['jwt'], acceptedAuthModes = [AUTH_MODE_JWT],
requiredServiceTokenPermissions = []
}: { }: {
acceptedAuthModes: string[]; acceptedAuthModes: string[];
requiredServiceTokenPermissions?: 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 { authTokenType, authTokenValue } = validateAuthMode({ const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers, headers: req.headers,
acceptedAuthModes acceptedAuthModes
}); });
// attach auth payloads let authPayload: IUser | IServiceAccount | IServiceTokenData;
let serviceTokenData: any; switch (authMode) {
switch (authTokenType) { case AUTH_MODE_SERVICE_ACCOUNT:
case 'serviceToken': authPayload = await getAuthSAAKPayload({
serviceTokenData = await getAuthSTDPayload({
authTokenValue authTokenValue
}); });
req.serviceAccount = authPayload;
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
break; break;
case 'apiKey': case AUTH_MODE_SERVICE_TOKEN:
req.user = await getAuthAPIKeyPayload({ authPayload = await getAuthSTDPayload({
authTokenValue authTokenValue
}); });
req.serviceTokenData = authPayload;
break;
case AUTH_MODE_API_KEY:
authPayload = await getAuthAPIKeyPayload({
authTokenValue
});
req.user = authPayload;
break; break;
default: default:
req.user = await getAuthUserPayload({ authPayload = await getAuthUserPayload({
authTokenValue authTokenValue
}); });
req.user = authPayload;
break; break;
} }
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload // User, ServiceAccount, ServiceTokenData
}
return next(); return next();
} }
} }
+9 -13
View File
@@ -1,32 +1,28 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Bot } from '../models'; import { Bot } from '../models';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { validateClientForBot } from '../helpers/bot';
import { AccountNotFoundError } from '../utils/errors'; import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
const requireBotAuth = ({ const requireBotAuth = ({
acceptedRoles, acceptedRoles,
location = 'params' locationBotId = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
location?: req; locationBotId?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const bot = await Bot.findById(req[location].botId); const { botId } = req[locationBotId];
if (!bot) { req.bot = await validateClientForBot({
return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) authData: req.authData,
} botId: new Types.ObjectId(botId),
await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles acceptedRoles
}); });
req.bot = bot;
next(); next();
} }
} }
@@ -1,7 +1,9 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Integration, IntegrationAuth } from '../models'; import { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services'; import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { validateClientForIntegration } from '../helpers/integration';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors'; import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/** /**
@@ -13,42 +15,24 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err
const requireIntegrationAuth = ({ const requireIntegrationAuth = ({
acceptedRoles acceptedRoles
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware
const { integrationId } = req.params; const { integrationId } = req.params;
// validate integration accessibility const { integration, accessToken } = await validateClientForIntegration({
const integration = await Integration.findOne({ authData: req.authData,
_id: integrationId integrationId: new Types.ObjectId(integrationId),
});
if (!integration) {
return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(),
acceptedRoles acceptedRoles
}); });
const integrationAuth = await IntegrationAuth.findOne({ if (integration) {
_id: integration.integrationAuth req.integration = integration;
}).select( }
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
); if (accessToken) {
req.accessToken = accessToken;
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}))
} }
req.integration = integration;
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
return next(); return next();
}; };
@@ -1,7 +1,9 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models'; import { IntegrationAuth, IWorkspace } from '../models';
import { IntegrationService } from '../services'; import { IntegrationService } from '../services';
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
@@ -19,36 +21,26 @@ const requireIntegrationAuthorizationAuth = ({
attachAccessToken = true, attachAccessToken = true,
location = 'params' location = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean; attachAccessToken?: boolean;
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const { integrationAuthId } = req[location]; const { integrationAuthId } = req[location];
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
})
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) { const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'})) authData: req.authData,
} integrationAuthId: new Types.ObjectId(integrationAuthId),
acceptedRoles,
await validateMembership({ attachAccessToken
userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace._id.toString(),
acceptedRoles
}); });
if (integrationAuth) {
req.integrationAuth = integrationAuth;
}
req.integrationAuth = integrationAuth; if (accessToken) {
if (attachAccessToken) { req.accessToken = accessToken;
const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
req.accessToken = access.accessToken;
} }
return next(); return next();
+17 -31
View File
@@ -1,9 +1,13 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
import { import {
Membership, Membership,
} from '../models'; } from '../models';
import { validateMembership } from '../helpers/membership'; import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -16,43 +20,25 @@ type req = 'params' | 'body' | 'query';
*/ */
const requireMembershipAuth = ({ const requireMembershipAuth = ({
acceptedRoles, acceptedRoles,
location = 'params' locationMembershipId = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
location?: req; locationMembershipId: req
}) => { }) => {
return async ( return async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
try { const { membershipId } = req[locationMembershipId];
const { membershipId } = req[location];
req.targetMembership = await validateClientForMembership({
const membership = await Membership.findById(membershipId); authData: req.authData,
membershipId: new Types.ObjectId(membershipId),
if (!membership) throw new Error('Failed to find target membership'); acceptedRoles
});
const userMembership = await Membership.findOne({
workspace: membership.workspace return next();
});
if (!userMembership) throw new Error('Failed to validate own membership')
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
workspaceId: membership.workspace.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate workspace membership'
}));
}
} }
} }
@@ -1,11 +1,17 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
import { import {
MembershipOrg MembershipOrg
} from '../models'; } from '../models';
import { validateMembership } from '../helpers/membershipOrg'; import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
// TODO: transform
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
/** /**
@@ -17,32 +23,24 @@ type req = 'params' | 'body' | 'query';
*/ */
const requireMembershipOrgAuth = ({ const requireMembershipOrgAuth = ({
acceptedRoles, acceptedRoles,
location = 'params' acceptedStatuses,
locationMembershipOrgId = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'owner' | 'admin' | 'member'>;
location?: req; acceptedStatuses: Array<'invited' | 'accepted'>;
locationMembershipOrgId?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const { membershipId } = req[locationMembershipOrgId];
const { membershipId } = req[location];
const membershipOrg = await MembershipOrg.findById(membershipId); req.membershipOrg = await validateClientForMembershipOrg({
authData: req.authData,
if (!membershipOrg) throw new Error('Failed to find target organization membership'); membershipOrgId: new Types.ObjectId(membershipId),
acceptedRoles,
const targetMembership = await validateMembership({ acceptedStatuses
userId: req.user._id.toString(), });
organizationId: membershipOrg.organization.toString(),
acceptedRoles return next();
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate organization membership'
}));
}
} }
} }
@@ -1,45 +1,46 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { IOrganization, MembershipOrg } from '../models'; import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors'; import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { validateMembershipOrg } from '../helpers/membershipOrg';
import { validateClientForOrganization } from '../helpers/organization';
type req = 'params' | 'body' | 'query';
/** /**
* Validate if user on request is a member with proper roles for organization * Validate if user on request is a member with proper roles for organization
* on request params. * on request params.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted organization roles * @param {String[]} obj.acceptedRoles - accepted organization roles
* @param {String[]} obj.acceptedStatuses - accepted organization statuses * @param {String[]} obj.accepteStatuses - accepted organization statuses
*/ */
const requireOrganizationAuth = ({ const requireOrganizationAuth = ({
acceptedRoles, acceptedRoles,
acceptedStatuses acceptedStatuses,
locationOrganizationId = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: string[]; acceptedStatuses: Array<'invited' | 'accepted'>;
locationOrganizationId?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// organization authorization middleware const { organizationId } = req[locationOrganizationId];
// validate organization membership const { organization, membershipOrg } = await validateClientForOrganization({
const membershipOrg = await MembershipOrg.findOne({ authData: req.authData,
user: req.user._id, organizationId: new Types.ObjectId(organizationId),
organization: req.params.organizationId acceptedRoles,
}).populate<{ organization: IOrganization }>('organization'); acceptedStatuses
});
if (!membershipOrg) { if (organization) {
return next(UnauthorizedRequestError({message: "You're not a member of this Organization."})) req.organization = organization;
}
//TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101
if (!acceptedRoles.includes(membershipOrg.role)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Role'}))
} }
if (!acceptedStatuses.includes(membershipOrg.status)) { if (membershipOrg) {
return next(ValidationError({message: 'Failed to validate Organization Membership Status'})) req.membershipOrg = membershipOrg;
} }
req.membershipOrg = membershipOrg;
return next(); return next();
}; };
}; };
+21 -25
View File
@@ -1,12 +1,17 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
import { Secret } from '../models'; import { Secret } from '../models';
import { import {
validateMembership validateMembership
} from '../helpers/membership'; } 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 // newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception
// of some /ee endpoints
/** /**
* Validate if user on request has proper membership to modify secret. * Validate if user on request has proper membership to modify secret.
@@ -15,34 +20,25 @@ import {
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/ */
const requireSecretAuth = ({ const requireSecretAuth = ({
acceptedRoles acceptedRoles,
requiredPermissions
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const { secretId } = req.params;
const { secretId } = req.params;
const secret = await validateClientForSecret({
const secret = await Secret.findById(secretId); authData: req.authData,
secretId: new Types.ObjectId(secretId),
if (!secret) { acceptedRoles,
return next(SecretNotFoundError({ requiredPermissions
message: 'Failed to find secret' });
}));
} req._secret = secret;
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secret.workspace.toString(),
acceptedRoles
});
req._secret = secret;
next(); next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' }));
}
} }
} }
+22 -35
View File
@@ -1,48 +1,35 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models'; import { Secret, Membership } from '../models';
import { validateSecrets } from '../helpers/secret'; import { validateClientForSecrets } from '../helpers/secrets';
// TODO: make this work for delete route
const requireSecretsAuth = ({ const requireSecretsAuth = ({
acceptedRoles acceptedRoles,
requiredPermissions = []
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
requiredPermissions?: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
let secrets; let secretIds = [];
try { if (Array.isArray(req.body.secrets)) {
if (Array.isArray(req.body.secrets)) { secretIds = req.body.secrets.map((s: any) => s.id);
// case: validate multiple secrets } else if (typeof req.body.secrets === 'object') {
secrets = await validateSecrets({ secretIds = [req.body.secrets.id];
userId: req.user._id.toString(), } else if (Array.isArray(req.body.secretIds)) {
secretIds: req.body.secrets.map((s: any) => s.id) secretIds = req.body.secretIds;
}); } else if (typeof req.body.secretIds === 'string') {
} else if (typeof req.body.secrets === 'object') { // change this to check for object secretIds = [req.body.secretIds];
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secrets.id]
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secretIds
});
} else if (typeof req.body.secretIds === 'string') {
// case: validate secretIds
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secretIds]
});
}
req.secrets = secrets;
return next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
} }
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions
});
return next();
} }
} }
@@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceAccount } from '../models';
import {
ServiceAccountNotFoundError
} from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
validateClientForServiceAccount
} from '../helpers/serviceAccount';
type req = 'params' | 'body' | 'query';
const requireServiceAccountAuth = ({
acceptedRoles,
acceptedStatuses,
locationServiceAccountId = 'params',
requiredPermissions = []
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
locationServiceAccountId?: req;
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const serviceAccountId = req[locationServiceAccountId].serviceAccountId;
req.serviceAccount = await validateClientForServiceAccount({
authData: req.authData,
serviceAccountId: new Types.ObjectId(serviceAccountId),
requiredPermissions
});
next();
}
}
export default requireServiceAccountAuth;
@@ -0,0 +1,52 @@
import { Request, Response, NextFunction } from 'express';
import { ServiceAccount, ServiceAccountWorkspacePermission } from '../models';
import {
ServiceAccountNotFoundError
} from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
type req = 'params' | 'body' | 'query';
const requireServiceAccountWorkspacePermissionAuth = ({
acceptedRoles,
acceptedStatuses,
location = 'params'
}: {
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const serviceAccountWorkspacePermissionId = req[location].serviceAccountWorkspacePermissionId;
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findById(serviceAccountWorkspacePermissionId);
if (!serviceAccountWorkspacePermission) {
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account workspace permission' }));
}
const serviceAccount = await ServiceAccount.findById(serviceAccountWorkspacePermission.serviceAccount);
if (!serviceAccount) {
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' }));
}
if (serviceAccount.user.toString() !== req.user.id.toString()) {
// case: creator of the service account is different from
// the user on the request -> apply middleware role/status validation
await validateMembershipOrg({
userId: req.user._id,
organizationId: serviceAccount.organization,
acceptedRoles,
acceptedStatuses
});
}
req.serviceAccount = serviceAccount;
next();
}
}
export default requireServiceAccountWorkspacePermissionAuth;
@@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceToken, ServiceTokenData } from '../models'; import { ServiceToken, ServiceTokenData } from '../models';
import { validateClientForServiceTokenData } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors'; import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
@@ -9,30 +11,17 @@ const requireServiceTokenDataAuth = ({
acceptedRoles, acceptedRoles,
location = 'params' location = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const { serviceTokenDataId } = req[location]; const { serviceTokenDataId } = req[location];
const serviceTokenData = await ServiceTokenData req.serviceTokenData = await validateClientForServiceTokenData({
.findById(req[location].serviceTokenDataId) authData: req.authData,
.select('+encryptedKey +iv +tag').populate('user'); serviceTokenDataId: new Types.ObjectId(serviceTokenDataId),
acceptedRoles
if (!serviceTokenData) { });
return next(AccountNotFoundError({ message: 'Failed to locate service token data' }));
}
if (req.user) {
// case: jwt auth
await validateMembership({
userId: req.user._id.toString(),
workspaceId: serviceTokenData.workspace.toString(),
acceptedRoles
});
}
req.serviceTokenData = serviceTokenData;
next(); next();
} }
+25 -28
View File
@@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { validateClientForWorkspace } from '../helpers/workspace';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -13,38 +15,33 @@ type req = 'params' | 'body' | 'query';
*/ */
const requireWorkspaceAuth = ({ const requireWorkspaceAuth = ({
acceptedRoles, acceptedRoles,
location = 'params' locationWorkspaceId,
locationEnvironment = undefined,
requiredPermissions = []
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
location?: req; locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const workspaceId = req[locationWorkspaceId]?.workspaceId;
const { workspaceId } = req[location]; const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
if (req.user) { // validate clients
// case: jwt auth const { membership } = await validateClientForWorkspace({
const membership = await validateMembership({ authData: req.authData,
userId: req.user._id.toString(), workspaceId: new Types.ObjectId(workspaceId),
workspaceId, environment,
acceptedRoles acceptedRoles,
}); requiredPermissions
});
req.membership = membership;
} if (membership) {
req.membership = membership;
if (
req.serviceTokenData
&& req.serviceTokenData.workspace.toString() !== workspaceId
&& req.serviceTokenData.environment !== req.body.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
return next();
} catch (err) {
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
} }
return next();
}; };
}; };
+4
View File
@@ -3,6 +3,7 @@ import { Schema, model, Types } from 'mongoose';
export interface IAPIKeyData { export interface IAPIKeyData {
name: string; name: string;
user: Types.ObjectId; user: Types.ObjectId;
lastUsed: Date;
expiresAt: Date; expiresAt: Date;
secretHash: string; secretHash: string;
} }
@@ -18,6 +19,9 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
ref: 'User', ref: 'User',
required: true required: true
}, },
lastUsed: {
type: Date
},
expiresAt: { expiresAt: {
type: Date type: Date
}, },
+12
View File
@@ -10,6 +10,10 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
import Organization, { IOrganization } from './organization'; import Organization, { IOrganization } from './organization';
import Secret, { ISecret } from './secret'; import Secret, { ISecret } from './secret';
import ServiceToken, { IServiceToken } from './serviceToken'; import ServiceToken, { IServiceToken } from './serviceToken';
import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new
import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new
import TokenData, { ITokenData } from './tokenData'; import TokenData, { ITokenData } from './tokenData';
import User, { IUser } from './user'; import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction'; import UserAction, { IUserAction } from './userAction';
@@ -43,6 +47,14 @@ export {
ISecret, ISecret,
ServiceToken, ServiceToken,
IServiceToken, IServiceToken,
ServiceAccount,
IServiceAccount,
ServiceAccountKey,
IServiceAccountKey,
ServiceAccountOrganizationPermission,
IServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission,
IServiceAccountWorkspacePermission,
TokenData, TokenData,
ITokenData, ITokenData,
User, User,
+26 -2
View File
@@ -9,9 +9,11 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -20,9 +22,12 @@ export interface IIntegration {
environment: string; environment: string;
isActive: boolean; isActive: boolean;
app: string; app: string;
appId: string;
owner: string; owner: string;
targetEnvironment: string; targetEnvironment: string;
appId: string; targetEnvironmentId: string;
targetService: string;
targetServiceId: string;
path: string; path: string;
region: string; region: string;
integration: integration:
@@ -35,9 +40,11 @@ export interface IIntegration {
| 'github' | 'github'
| 'gitlab' | 'gitlab'
| 'render' | 'render'
| 'railway'
| 'flyio' | 'flyio'
| 'circleci' | 'circleci'
| 'travisci'; | 'travisci'
| 'supabase';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -71,6 +78,20 @@ const integrationSchema = new Schema<IIntegration>(
type: String, type: String,
default: null, default: null,
}, },
targetEnvironmentId: {
type: String,
default: null
},
targetService: {
// railway-specific service
type: String,
default: null
},
targetServiceId: {
// railway-specific service
type: String,
default: null
},
owner: { owner: {
// github-specific repo owner-login // github-specific repo owner-login
type: String, type: String,
@@ -78,6 +99,7 @@ const integrationSchema = new Schema<IIntegration>(
}, },
path: { path: {
// aws-parameter-store-specific path // aws-parameter-store-specific path
// (also) vercel preview-branch
type: String, type: String,
default: null default: null
}, },
@@ -98,9 +120,11 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
], ],
required: true, required: true,
}, },
+7 -3
View File
@@ -1,4 +1,4 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types, Document } from "mongoose";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -9,15 +9,17 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
} from "../variables"; } from "../variables";
export interface IIntegrationAuth { export interface IIntegrationAuth extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager'; integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager';
teamId: string; teamId: string;
accountId: string; accountId: string;
refreshCiphertext?: string; refreshCiphertext?: string;
@@ -51,9 +53,11 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
], ],
required: true, required: true,
}, },
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types, Document } from 'mongoose';
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables'; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables';
export interface IMembershipOrg { export interface IMembershipOrg extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail: string; inviteEmail: string;
+53
View File
@@ -0,0 +1,53 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccount extends Document {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
}
const serviceAccountSchema = new Schema<IServiceAccount>(
{
name: {
type: String,
required: true
},
organization: {
type: Schema.Types.ObjectId,
ref: 'Organization',
required: true
},
user: { // user who created the service account
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
publicKey: {
type: String,
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
}
},
{
timestamps: true
}
);
const ServiceAccount = model<IServiceAccount>('ServiceAccount', serviceAccountSchema);
export default ServiceAccount;
+44
View File
@@ -0,0 +1,44 @@
import { Schema, model, Types } from 'mongoose';
export interface IServiceAccountKey {
_id: Types.ObjectId;
encryptedKey: string;
nonce: string;
sender: Types.ObjectId;
serviceAccount: Types.ObjectId;
workspace: Types.ObjectId;
}
const serviceAccountKeySchema = new Schema<IServiceAccountKey>(
{
encryptedKey: {
type: String,
required: true
},
nonce: {
type: String,
required: true
},
sender: {
type: Schema.Types.ObjectId,
required: true
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
}
},
{
timestamps: true
}
);
const ServiceAccountKey = model<IServiceAccountKey>('ServiceAccountKey', serviceAccountKeySchema);
export default ServiceAccountKey;
@@ -0,0 +1,23 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccountOrganizationPermission extends Document {
_id: Types.ObjectId;
serviceAccount: Types.ObjectId;
}
const serviceAccountOrganizationPermissionSchema = new Schema<IServiceAccountOrganizationPermission>(
{
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
}
},
{
timestamps: true
}
);
const ServiceAccountOrganizationPermission = model<IServiceAccountOrganizationPermission>('ServiceAccountOrganizationPermission', serviceAccountOrganizationPermissionSchema);
export default ServiceAccountOrganizationPermission;
@@ -0,0 +1,44 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccountWorkspacePermission extends Document {
_id: Types.ObjectId;
serviceAccount: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
read: boolean;
write: boolean;
}
const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorkspacePermission>(
{
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
},
workspace:{
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
required: true
},
read: {
type: Boolean,
default: false
},
write: {
type: Boolean,
default: false
}
},
{
timestamps: true
}
);
const ServiceAccountWorkspacePermission = model<IServiceAccountWorkspacePermission>('ServiceAccountWorkspacePermission', serviceAccountWorkspacePermissionSchema);
export default ServiceAccountWorkspacePermission;
+14 -5
View File
@@ -1,10 +1,13 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceTokenData { export interface IServiceTokenData extends Document {
_id: Types.ObjectId;
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; environment: string;
user: Types.ObjectId; user: Types.ObjectId;
serviceAccount: Types.ObjectId;
lastUsed: Date;
expiresAt: Date; expiresAt: Date;
secretHash: string; secretHash: string;
encryptedKey: string; encryptedKey: string;
@@ -24,14 +27,20 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
ref: 'Workspace', ref: 'Workspace',
required: true required: true
}, },
environment: { // TODO: adapt to upcoming environment id environment: {
type: String, type: String,
required: true required: true
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: 'User'
required: true },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
lastUsed: {
type: Date
}, },
expiresAt: { expiresAt: {
type: Date type: Date
+3 -2
View File
@@ -4,6 +4,7 @@ import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v1'; import { authController } from '../../controllers/v1';
import { authLimiter } from '../../helpers/rateLimiter'; import { authLimiter } from '../../helpers/rateLimiter';
import { AUTH_MODE_JWT } from '../../variables';
router.post('/token', validateRequest, authController.getNewToken); router.post('/token', validateRequest, authController.getNewToken);
@@ -29,7 +30,7 @@ router.post(
'/logout', '/logout',
authLimiter, authLimiter,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
authController.logout authController.logout
); );
@@ -37,7 +38,7 @@ router.post(
router.post( router.post(
'/checkAuth', '/checkAuth',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
authController.checkAuth authController.checkAuth
); );
+5 -4
View File
@@ -8,15 +8,16 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { botController } from '../../controllers/v1'; import { botController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../variables'; import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables';
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim().notEmpty(), param('workspaceId').exists().trim().notEmpty(),
validateRequest, validateRequest,
@@ -26,7 +27,7 @@ router.get(
router.patch( router.patch(
'/:botId/active', '/:botId/active',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireBotAuth({ requireBotAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
+12 -4
View File
@@ -6,14 +6,19 @@ import {
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1'; import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration for integration auth router.post( // new: add new integration for integration auth
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -25,6 +30,9 @@ router.post( // new: add new integration for integration auth
body('appId').trim(), body('appId').trim(),
body('sourceEnvironment').trim(), body('sourceEnvironment').trim(),
body('targetEnvironment').trim(), body('targetEnvironment').trim(),
body('targetEnvironmentId').trim(),
body('targetService').trim(),
body('targetServiceId').trim(),
body('owner').trim(), body('owner').trim(),
body('path').trim(), body('path').trim(),
body('region').trim(), body('region').trim(),
@@ -35,7 +43,7 @@ router.post( // new: add new integration for integration auth
router.patch( router.patch(
'/:integrationId', '/:integrationId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -54,7 +62,7 @@ router.patch(
router.delete( router.delete(
'/:integrationId', '/:integrationId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
+63 -15
View File
@@ -7,13 +7,18 @@ import {
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { integrationAuthController } from '../../controllers/v1'; import { integrationAuthController } from '../../controllers/v1';
router.get( router.get(
'/integration-options', '/integration-options',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
integrationAuthController.getIntegrationOptions integrationAuthController.getIntegrationOptions
); );
@@ -21,7 +26,7 @@ router.get(
router.get( router.get(
'/:integrationAuthId', '/:integrationAuthId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -34,11 +39,11 @@ router.get(
router.post( router.post(
'/oauth-token', '/oauth-token',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' locationWorkspaceId: 'body'
}), }),
body('workspaceId').exists().trim().notEmpty(), body('workspaceId').exists().trim().notEmpty(),
body('code').exists().trim().notEmpty(), body('code').exists().trim().notEmpty(),
@@ -49,25 +54,25 @@ router.post(
router.post( router.post(
'/access-token', '/access-token',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('workspaceId').exists().trim().notEmpty(), body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(), body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(), body('accessToken').exists().trim().notEmpty(),
body('integration').exists().trim().notEmpty(), body('integration').exists().trim().notEmpty(),
validateRequest, validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
}),
integrationAuthController.saveIntegrationAccessToken integrationAuthController.saveIntegrationAccessToken
); );
router.get( router.get(
'/:integrationAuthId/apps', '/:integrationAuthId/apps',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -81,7 +86,7 @@ router.get(
router.get( router.get(
'/:integrationAuthId/teams', '/:integrationAuthId/teams',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -91,10 +96,53 @@ router.get(
integrationAuthController.getIntegrationAuthTeams integrationAuthController.getIntegrationAuthTeams
); );
router.get(
'/:integrationAuthId/vercel/branches',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
query('teamId').optional().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthVercelBranches
);
router.get(
'/:integrationAuthId/railway/environments',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthRailwayEnvironments
);
router.get(
'/:integrationAuthId/railway/services',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthRailwayServices
);
router.delete( router.delete(
'/:integrationAuthId', '/:integrationAuthId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
+2 -1
View File
@@ -3,11 +3,12 @@ const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../../controllers/v1'; import { membershipOrgController } from '../../controllers/v1';
import { AUTH_MODE_JWT } from '../../variables';
router.post( router.post(
'/signup', '/signup',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('inviteeEmail').exists().trim().notEmpty().isEmail(), body('inviteeEmail').exists().trim().notEmpty().isEmail(),
body('organizationId').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(),
+7 -5
View File
@@ -6,16 +6,17 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables'; import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables';
import { keyController } from '../../controllers/v1'; import { keyController } from '../../controllers/v1';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('key').exists(), body('key').exists(),
@@ -26,10 +27,11 @@ router.post(
router.get( router.get(
'/:workspaceId/latest', '/:workspaceId/latest',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId'), param('workspaceId'),
validateRequest, validateRequest,
+5 -4
View File
@@ -4,13 +4,14 @@ import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../../controllers/v1'; import { membershipController } from '../../controllers/v1';
import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1'; import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1';
import { AUTH_MODE_JWT } from '../../variables';
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
router.get( // used for old CLI (deprecate) router.get( // used for old CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -20,7 +21,7 @@ router.get( // used for old CLI (deprecate)
router.delete( router.delete(
'/:membershipId', '/:membershipId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
@@ -30,7 +31,7 @@ router.delete(
router.post( router.post(
'/:membershipId/change-role', '/:membershipId/change-role',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('role').exists().trim(), body('role').exists().trim(),
validateRequest, validateRequest,
@@ -40,7 +41,7 @@ router.post(
router.post( router.post(
'/:membershipId/deny-permissions', '/:membershipId/deny-permissions',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('membershipId').isMongoId().exists().trim(), param('membershipId').isMongoId().exists().trim(),
body('permissions').isArray().exists(), body('permissions').isArray().exists(),
+3 -2
View File
@@ -3,12 +3,13 @@ const router = express.Router();
import { param } from 'express-validator'; import { param } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../../controllers/v1'; import { membershipOrgController } from '../../controllers/v1';
import { AUTH_MODE_JWT } from '../../variables';
router.post( router.post(
// TODO // TODO
'/membershipOrg/:membershipOrgId/change-role', '/membershipOrg/:membershipOrgId/change-role',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('membershipOrgId'), param('membershipOrgId'),
validateRequest, validateRequest,
@@ -18,7 +19,7 @@ router.post(
router.delete( router.delete(
'/:membershipOrgId', '/:membershipOrgId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('membershipOrgId').exists().trim(), param('membershipOrgId').exists().trim(),
validateRequest, validateRequest,
+19 -13
View File
@@ -6,13 +6,19 @@ import {
requireOrganizationAuth, requireOrganizationAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED,
AUTH_MODE_JWT
} from '../../variables';
import { organizationController } from '../../controllers/v1'; import { organizationController } from '../../controllers/v1';
router.get( // deprecated (moved to api/v2/users/me/organizations) router.get( // deprecated (moved to api/v2/users/me/organizations)
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
organizationController.getOrganizations organizationController.getOrganizations
); );
@@ -20,7 +26,7 @@ router.get( // deprecated (moved to api/v2/users/me/organizations)
router.post( // not used on frontend router.post( // not used on frontend
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('organizationName').exists().trim().notEmpty(), body('organizationName').exists().trim().notEmpty(),
validateRequest, validateRequest,
@@ -30,7 +36,7 @@ router.post( // not used on frontend
router.get( router.get(
'/:organizationId', '/:organizationId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -44,7 +50,7 @@ router.get(
router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships) router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships)
'/:organizationId/users', '/:organizationId/users',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -58,7 +64,7 @@ router.get( // deprecated (moved to api/v2/organizations/:organizationId/members
router.get( router.get(
'/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces) '/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -72,7 +78,7 @@ router.get(
router.patch( router.patch(
'/:organizationId/name', '/:organizationId/name',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -87,7 +93,7 @@ router.patch(
router.get( router.get(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -101,7 +107,7 @@ router.get(
router.post( router.post(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -116,7 +122,7 @@ router.post(
router.delete( router.delete(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -131,7 +137,7 @@ router.delete(
router.post( router.post(
'/:organizationId/customer-portal-session', '/:organizationId/customer-portal-session',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -145,7 +151,7 @@ router.post(
router.get( router.get(
'/:organizationId/subscriptions', '/:organizationId/subscriptions',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -159,7 +165,7 @@ router.get(
router.get( router.get(
'/:organizationId/workspace-memberships', '/:organizationId/workspace-memberships',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
+6 -3
View File
@@ -4,11 +4,14 @@ import { body } from 'express-validator';
import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware'; import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
import { passwordController } from '../../controllers/v1'; import { passwordController } from '../../controllers/v1';
import { passwordLimiter } from '../../helpers/rateLimiter'; import { passwordLimiter } from '../../helpers/rateLimiter';
import {
AUTH_MODE_JWT
} from '../../variables';
router.post( router.post(
'/srp1', '/srp1',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('clientPublicKey').exists().isString().trim().notEmpty(), body('clientPublicKey').exists().isString().trim().notEmpty(),
validateRequest, validateRequest,
@@ -19,7 +22,7 @@ router.post(
'/change-password', '/change-password',
passwordLimiter, passwordLimiter,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('clientProof').exists().trim().notEmpty(), body('clientProof').exists().trim().notEmpty(),
body('protectedKey').exists().isString().trim().notEmpty(), body('protectedKey').exists().isString().trim().notEmpty(),
@@ -62,7 +65,7 @@ router.post(
'/backup-private-key', '/backup-private-key',
passwordLimiter, passwordLimiter,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('clientProof').exists().isString().trim().notEmpty(), body('clientProof').exists().isString().trim().notEmpty(),
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key
+13 -5
View File
@@ -8,15 +8,22 @@ import {
} from '../../middleware'; } from '../../middleware';
import { body, query, param } from 'express-validator'; import { body, query, param } from 'express-validator';
import { secretController } from '../../controllers/v1'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
// note to devs: these endpoints will be deprecated in favor of v2
router.post( router.post(
'/:workspaceId', '/:workspaceId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
body('secrets').exists(), body('secrets').exists(),
body('keys').exists(), body('keys').exists(),
@@ -30,10 +37,11 @@ router.post(
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
query('environment').exists().trim(), query('environment').exists().trim(),
query('channel'), query('channel'),
+7 -3
View File
@@ -7,7 +7,11 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { body } from 'express-validator'; import { body } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
import { serviceTokenController } from '../../controllers/v1'; import { serviceTokenController } from '../../controllers/v1';
// note: deprecate service-token routes in favor of service-token data routes/structure // note: deprecate service-token routes in favor of service-token data routes/structure
@@ -21,11 +25,11 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' locationWorkspaceId: 'body'
}), }),
body('name').exists().trim().notEmpty(), body('name').exists().trim().notEmpty(),
body('workspaceId').exists().trim().notEmpty(), body('workspaceId').exists().trim().notEmpty(),
+4 -1
View File
@@ -2,11 +2,14 @@ import express from 'express';
const router = express.Router(); const router = express.Router();
import { requireAuth } from '../../middleware'; import { requireAuth } from '../../middleware';
import { userController } from '../../controllers/v1'; import { userController } from '../../controllers/v1';
import {
AUTH_MODE_JWT
} from '../../variables';
router.get( router.get(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
userController.getUser userController.getUser
); );
+3 -2
View File
@@ -3,12 +3,13 @@ const router = express.Router();
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator'; import { body, query } from 'express-validator';
import { userActionController } from '../../controllers/v1'; import { userActionController } from '../../controllers/v1';
import { AUTH_MODE_JWT } from '../../variables';
// note: [userAction] will be deprecated in /v2 in favor of [action] // note: [userAction] will be deprecated in /v2 in favor of [action]
router.post( router.post(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('action'), body('action'),
validateRequest, validateRequest,
@@ -18,7 +19,7 @@ router.post(
router.get( router.get(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
query('action'), query('action'),
validateRequest, validateRequest,
+33 -20
View File
@@ -6,16 +6,21 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
import { workspaceController, membershipController } from '../../controllers/v1'; import { workspaceController, membershipController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/keys', '/:workspaceId/keys',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -25,10 +30,11 @@ router.get(
router.get( router.get(
'/:workspaceId/users', '/:workspaceId/users',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -38,7 +44,7 @@ router.get(
router.get( router.get(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
workspaceController.getWorkspaces workspaceController.getWorkspaces
); );
@@ -46,10 +52,11 @@ router.get(
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -59,7 +66,7 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('workspaceName').exists().trim().notEmpty(), body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(),
@@ -70,10 +77,11 @@ router.post(
router.delete( router.delete(
'/:workspaceId', '/:workspaceId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN] acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -83,10 +91,11 @@ router.delete(
router.post( router.post(
'/:workspaceId/name', '/:workspaceId/name',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('name').exists().trim().notEmpty(), body('name').exists().trim().notEmpty(),
@@ -97,10 +106,11 @@ router.post(
router.post( router.post(
'/:workspaceId/invite-signup', '/:workspaceId/invite-signup',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('email').exists().trim().notEmpty(), body('email').exists().trim().notEmpty(),
@@ -111,10 +121,11 @@ router.post(
router.get( router.get(
'/:workspaceId/integrations', '/:workspaceId/integrations',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -124,10 +135,11 @@ router.get(
router.get( router.get(
'/:workspaceId/authorizations', '/:workspaceId/authorizations',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -137,10 +149,11 @@ router.get(
router.get( router.get(
'/:workspaceId/service-tokens', // deprecate '/:workspaceId/service-tokens', // deprecate
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
+7 -4
View File
@@ -1,16 +1,19 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { param, body } from 'express-validator';
import { import {
requireAuth, requireAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { param, body } from 'express-validator';
import { apiKeyDataController } from '../../controllers/v2'; import { apiKeyDataController } from '../../controllers/v2';
import {
AUTH_MODE_JWT
} from '../../variables';
router.get( router.get(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
apiKeyDataController.getAPIKeyData apiKeyDataController.getAPIKeyData
); );
@@ -18,7 +21,7 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('name').exists().trim(), body('name').exists().trim(),
body('expiresIn'), // measured in ms body('expiresIn'), // measured in ms
@@ -29,7 +32,7 @@ router.post(
router.delete( router.delete(
'/:apiKeyDataId', '/:apiKeyDataId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('apiKeyDataId').exists().trim(), param('apiKeyDataId').exists().trim(),
validateRequest, validateRequest,
+14 -6
View File
@@ -7,15 +7,20 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest,
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
router.post( router.post(
'/:workspaceId/environments', '/:workspaceId/environments',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'], acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -27,10 +32,11 @@ router.post(
router.put( router.put(
'/:workspaceId/environments', '/:workspaceId/environments',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'], acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -43,10 +49,11 @@ router.put(
router.delete( router.delete(
'/:workspaceId/environments', '/:workspaceId/environments',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'], acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -57,14 +64,15 @@ router.delete(
router.get( router.get(
'/:workspaceId/environments', '/:workspaceId/environments',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'], acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN], acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
environmentController.getAllAccessibleEnvironmentsOfWorkspace environmentController.getAllAccessibleEnvironmentsOfWorkspace
); );
export default router; export default router;
+2
View File
@@ -6,6 +6,7 @@ import workspace from './workspace';
import secret from './secret'; // deprecated import secret from './secret'; // deprecated
import secrets from './secrets'; import secrets from './secrets';
import serviceTokenData from './serviceTokenData'; import serviceTokenData from './serviceTokenData';
import serviceAccounts from './serviceAccounts';
import apiKeyData from './apiKeyData'; import apiKeyData from './apiKeyData';
import environment from "./environment" import environment from "./environment"
import tags from "./tags" import tags from "./tags"
@@ -19,6 +20,7 @@ export {
secret, secret,
secrets, secrets,
serviceTokenData, serviceTokenData,
serviceAccounts,
apiKeyData, apiKeyData,
environment, environment,
tags tags
+31 -8
View File
@@ -6,8 +6,15 @@ import {
requireMembershipOrgAuth, requireMembershipOrgAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { body, param, query } from 'express-validator'; import { body, param } from 'express-validator';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { organizationsController } from '../../controllers/v2'; import { organizationsController } from '../../controllers/v2';
// TODO: /POST to create membership // TODO: /POST to create membership
@@ -17,7 +24,7 @@ router.get(
param('organizationId').exists().trim(), param('organizationId').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -33,14 +40,15 @@ router.patch(
body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
}), }),
requireMembershipOrgAuth({ requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN] acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.updateOrganizationMembership organizationsController.updateOrganizationMembership
); );
@@ -51,14 +59,15 @@ router.delete(
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
}), }),
requireMembershipOrgAuth({ requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN] acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.deleteOrganizationMembership organizationsController.deleteOrganizationMembership
); );
@@ -68,7 +77,7 @@ router.get(
param('organizationId').exists().trim(), param('organizationId').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
@@ -77,4 +86,18 @@ router.get(
organizationsController.getOrganizationWorkspaces organizationsController.getOrganizationWorkspaces
); );
router.get(
'/:organizationId/service-accounts',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationServiceAccounts
);
export default router; export default router;
+32 -18
View File
@@ -6,7 +6,14 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { body, param, query } from 'express-validator'; import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables'; import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2'; import { secretController } from '../../controllers/v2';
@@ -17,10 +24,11 @@ const router = express.Router();
router.post( router.post(
'/batch-create/workspace/:workspaceId/environment/:environment', '/batch-create/workspace/:workspaceId/environment/:environment',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().isMongoId().trim(), param('workspaceId').exists().isMongoId().trim(),
param('environment').exists().trim(), param('environment').exists().trim(),
@@ -33,10 +41,11 @@ router.post(
router.post( router.post(
'/workspace/:workspaceId/environment/:environment', '/workspace/:workspaceId/environment/:environment',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().isMongoId().trim(), param('workspaceId').exists().isMongoId().trim(),
param('environment').exists().trim(), param('environment').exists().trim(),
@@ -51,10 +60,11 @@ router.get(
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
query("environment").exists(), query("environment").exists(),
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
query('channel'), query('channel'),
validateRequest, validateRequest,
@@ -64,10 +74,11 @@ router.get(
router.get( router.get(
'/:secretId', '/:secretId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken'] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
validateRequest, validateRequest,
secretController.getSecret secretController.getSecret
@@ -76,13 +87,14 @@ router.get(
router.delete( router.delete(
'/batch/workspace/:workspaceId/environment/:environmentName', '/batch/workspace/:workspaceId/environment/:environmentName',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
param('workspaceId').exists().isMongoId().trim(), param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(), param('environmentName').exists().trim(),
body('secretIds').exists().isArray().custom(array => array.length > 0), body('secretIds').exists().isArray().custom(array => array.length > 0),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
validateRequest, validateRequest,
secretController.deleteSecrets secretController.deleteSecrets
@@ -91,10 +103,11 @@ router.delete(
router.delete( router.delete(
'/:secretId', '/:secretId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}), }),
param('secretId').isMongoId(), param('secretId').isMongoId(),
validateRequest, validateRequest,
@@ -104,29 +117,30 @@ router.delete(
router.patch( router.patch(
'/batch-modify/workspace/:workspaceId/environment/:environmentName', '/batch-modify/workspace/:workspaceId/environment/:environmentName',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0),
param('workspaceId').exists().isMongoId().trim(), param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(), param('environmentName').exists().trim(),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
validateRequest, validateRequest,
secretController.updateSecrets secretController.updateSecrets
); );
router.patch( router.patch(
'/workspace/:workspaceId/environment/:environmentName', '/workspace/:workspaceId/environment/:environmentName',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
body('secret').isObject(), body('secret').isObject(),
param('workspaceId').exists().isMongoId().trim(), param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(), param('environmentName').exists().trim(),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
validateRequest, validateRequest,
secretController.updateSecret secretController.updateSecret
+30 -24
View File
@@ -1,5 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { Types } from 'mongoose';
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
@@ -8,12 +9,18 @@ import {
} from '../../middleware'; } from '../../middleware';
import { query, body } from 'express-validator'; import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2'; import { secretsController } from '../../controllers/v2';
import { validateSecrets } from '../../helpers/secret'; import { validateClientForSecrets } from '../../helpers/secrets';
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED SECRET_SHARED,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../../variables'; } from '../../variables';
import { import {
BatchSecretRequest BatchSecretRequest
@@ -22,12 +29,11 @@ import {
router.post( router.post(
'/batch', '/batch',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
requiredServiceTokenPermissions: ['read', 'write']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' locationWorkspaceId: 'body'
}), }),
body('workspaceId').exists().isString().trim(), body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(), body('environment').exists().isString().trim(),
@@ -40,12 +46,11 @@ router.post(
.filter((secretId) => secretId !== undefined) .filter((secretId) => secretId !== undefined)
if (secretIds.length > 0) { if (secretIds.length > 0) {
const relevantSecrets = await validateSecrets({ req.secrets = await validateClientForSecrets({
userId: req.user._id.toString(), authData: req.authData,
secretIds secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions: []
}); });
req.secrets = relevantSecrets;
} }
} }
return true; return true;
@@ -100,12 +105,13 @@ router.post(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
requiredServiceTokenPermissions: ['write']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
@@ -117,12 +123,13 @@ router.get(
query('tagSlugs'), query('tagSlugs'),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
requiredServiceTokenPermissions: ['read']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'query' locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -157,11 +164,11 @@ router.patch(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
requiredServiceTokenPermissions: ['write']
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.updateSecrets secretsController.updateSecrets
); );
@@ -186,14 +193,13 @@ router.delete(
.isEmpty(), .isEmpty(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
requiredServiceTokenPermissions: ['write']
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.deleteSecrets secretsController.deleteSecrets
); );
export default router; export default router;
+159
View File
@@ -0,0 +1,159 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
requireWorkspaceAuth,
requireServiceAccountAuth,
requireServiceAccountWorkspacePermissionAuth,
validateRequest
} from '../../middleware';
import { param, query, body } from 'express-validator';
import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT
} from '../../variables';
import { serviceAccountsController } from '../../controllers/v2';
router.get( // TODO: check
'/me',
requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT]
}),
serviceAccountsController.getCurrentServiceAccount
);
router.get(
'/:serviceAccountId',
param('serviceAccountId').exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.getServiceAccountById
);
router.post(
'/',
body('organizationId').exists().isString().trim(),
body('name').exists().isString().trim(),
body('publicKey').exists().isString().trim(),
body('expiresIn').isNumeric(), // measured in ms
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: 'body'
}),
serviceAccountsController.createServiceAccount
);
router.patch(
'/:serviceAccountId/name',
param('serviceAccountId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.changeServiceAccountName
);
router.delete(
'/:serviceAccountId',
param('serviceAccountId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.deleteServiceAccount
);
router.get(
'/:serviceAccountId/permissions/workspace',
param('serviceAccountId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.getServiceAccountWorkspacePermissions
);
router.post(
'/:serviceAccountId/permissions/workspace',
param('serviceAccountId').exists().isString().trim(),
body('workspaceId').exists().isString().notEmpty(),
body('environment').exists().isString().notEmpty(),
body('read').isBoolean().optional(),
body('write').isBoolean().optional(),
body('encryptedKey').exists().isString().notEmpty(),
body('nonce').exists().isString().notEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
}),
serviceAccountsController.addServiceAccountWorkspacePermission
);
router.delete(
'/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId',
param('serviceAccountId').exists().isString().trim(),
param('serviceAccountWorkspacePermissionId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireServiceAccountWorkspacePermissionAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.deleteServiceAccountWorkspacePermission
);
router.get(
'/:serviceAccountId/keys',
query('workspaceId').optional().isString(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.getServiceAccountKeys
);
export default router;

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