Merge remote-tracking branch 'origin' into secrets-v3
@@ -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
|
||||||
|
|||||||
@@ -62,7 +62,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",
|
||||||
@@ -3629,9 +3629,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/jest": {
|
"node_modules/@types/jest": {
|
||||||
"version": "29.4.0",
|
"version": "29.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
|
||||||
"integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==",
|
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expect": "^29.0.0",
|
"expect": "^29.0.0",
|
||||||
@@ -15642,9 +15642,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jest": {
|
"@types/jest": {
|
||||||
"version": "29.4.0",
|
"version": "29.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
|
||||||
"integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==",
|
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"expect": "^29.0.0",
|
"expect": "^29.0.0",
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"@aws-sdk/client-secrets-manager": "^3.287.0",
|
"@aws-sdk/client-secrets-manager": "^3.287.0",
|
||||||
"@godaddy/terminus": "^4.11.2",
|
"@godaddy/terminus": "^4.11.2",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"@sentry/tracing": "^7.39.0",
|
|
||||||
"@sentry/node": "^7.40.0",
|
"@sentry/node": "^7.40.0",
|
||||||
|
"@sentry/tracing": "^7.39.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",
|
||||||
@@ -57,7 +57,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 +80,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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -550,7 +550,10 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
const workspaceId = req.query.workspaceId as string;
|
const workspaceId = req.query.workspaceId as string;
|
||||||
const environment = req.query.environment as string;
|
const environment = req.query.environment as string;
|
||||||
|
|
||||||
// tags logic
|
// secrets to return
|
||||||
|
let secrets: ISecret[] = [];
|
||||||
|
|
||||||
|
// query tags table to get all tags ids for the tag names for the given workspace
|
||||||
let tagIds = [];
|
let tagIds = [];
|
||||||
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
|
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
|
||||||
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
||||||
@@ -560,72 +563,71 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
return tag ? tag.id : null;
|
return tag ? tag.id : null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let secrets: ISecret[] = [];
|
|
||||||
|
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
// case: client authorization is via JWT
|
// case: client authorization is via JWT
|
||||||
|
const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
|
||||||
let hasWriteOnlyAccess
|
const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
|
||||||
if (!req.serviceTokenData) {
|
if (hasNoAccess) {
|
||||||
hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
|
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||||
const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
|
|
||||||
if (hasNoAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let secretQuery: any;
|
const secretQuery: any = {
|
||||||
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
workspace: workspaceId,
|
||||||
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
|
environment,
|
||||||
|
$or: [
|
||||||
const tagIds = _.map(tagNamesList, (tagName) => {
|
{ user: req.user._id }, // personal secrets for this user
|
||||||
const tag = _.find(workspaceFromDB, { slug: tagName });
|
{ user: { $exists: false } } // shared secrets from workspace
|
||||||
return tag ? tag.id : null;
|
]
|
||||||
});
|
}
|
||||||
|
|
||||||
secretQuery = {
|
if (tagIds.length > 0) {
|
||||||
workspace: workspaceId,
|
secretQuery.tags = { $in: tagIds };
|
||||||
environment,
|
|
||||||
$or: [
|
|
||||||
{ user: req.user._id },
|
|
||||||
{ user: { $exists: false } }
|
|
||||||
],
|
|
||||||
tags: { $in: tagIds },
|
|
||||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
secretQuery = {
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
$or: [
|
|
||||||
{ user: req.user._id },
|
|
||||||
{ user: { $exists: false } }
|
|
||||||
],
|
|
||||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasWriteOnlyAccess) {
|
if (hasWriteOnlyAccess) {
|
||||||
// (i.e. you don't get values to decrypt since you can only write)
|
// 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")
|
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags")
|
||||||
} else {
|
} else {
|
||||||
secrets = await Secret.find(secretQuery).populate("tags")
|
secrets = await Secret.find(secretQuery).populate("tags")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.serviceAccount || req.serviceTokenData) {
|
// case: client authorization is via service token
|
||||||
// case: client authorization is either via service account or service token
|
if (req.serviceTokenData) {
|
||||||
|
const userId = req.serviceTokenData.user._id
|
||||||
secrets = await Secret.find({
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
const secretQuery: any = {
|
||||||
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
user: {
|
$or: [
|
||||||
$exists: false
|
{ user: userId }, // personal secrets for this user
|
||||||
},
|
{ user: { $exists: false } } // shared secrets from workspace
|
||||||
...(tagIds.length > 0 ? { tags: { $in: tagIds } } : {}),
|
]
|
||||||
type: SECRET_SHARED
|
}
|
||||||
});
|
|
||||||
|
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'])
|
||||||
@@ -638,7 +640,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
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: req.user?._id,
|
userId: req.user?._id,
|
||||||
serviceAccountId: req.serviceAccount?._id,
|
serviceAccountId: req.serviceAccount?._id,
|
||||||
@@ -952,7 +954,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'delete secrets!!'
|
message: 'delete secrets!!'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
|
||||||
workspaceId: secretSnapshot.workspace,
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const getAuthSTDPayload = async ({
|
|||||||
if (!isMatch) throw UnauthorizedRequestError({
|
if (!isMatch) throw UnauthorizedRequestError({
|
||||||
message: 'Failed to authenticate service token'
|
message: 'Failed to authenticate service token'
|
||||||
});
|
});
|
||||||
|
|
||||||
serviceTokenData = await ServiceTokenData
|
serviceTokenData = await ServiceTokenData
|
||||||
.findOneAndUpdate({
|
.findOneAndUpdate({
|
||||||
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
|
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
|
||||||
@@ -157,8 +157,8 @@ const getAuthSTDPayload = async ({
|
|||||||
}, {
|
}, {
|
||||||
new: true
|
new: true
|
||||||
})
|
})
|
||||||
.select('+encryptedKey +iv +tag');
|
.select('+encryptedKey +iv +tag').populate('user serviceAccount');
|
||||||
|
|
||||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||||
|
|
||||||
return serviceTokenData;
|
return serviceTokenData;
|
||||||
@@ -176,20 +176,20 @@ const getAuthSAAKPayload = async ({
|
|||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||||
|
|
||||||
const serviceAccount = await ServiceAccount.findById(
|
const serviceAccount = await ServiceAccount.findById(
|
||||||
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
|
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
|
||||||
).select('+secretHash');
|
).select('+secretHash');
|
||||||
|
|
||||||
if (!serviceAccount) {
|
if (!serviceAccount) {
|
||||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
|
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
|
||||||
if (!result) throw UnauthorizedRequestError({
|
if (!result) throw UnauthorizedRequestError({
|
||||||
message: 'Failed to authenticate service account access key'
|
message: 'Failed to authenticate service account access key'
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceAccount;
|
return serviceAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ const getAuthAPIKeyPayload = async ({
|
|||||||
|
|
||||||
let apiKeyData = await APIKeyData
|
let apiKeyData = await APIKeyData
|
||||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||||
.populate<{user: IUser}>('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' });
|
||||||
@@ -232,13 +232,13 @@ const getAuthAPIKeyPayload = async ({
|
|||||||
}, {
|
}, {
|
||||||
new: true
|
new: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!apiKeyData) {
|
if (!apiKeyData) {
|
||||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findById(apiKeyData.user).select('+publicKey');
|
const user = await User.findById(apiKeyData.user).select('+publicKey');
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw AccountNotFoundError({
|
throw AccountNotFoundError({
|
||||||
message: 'Failed to find user'
|
message: 'Failed to find user'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -1,10 +1,106 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Membership, Key } from '../models';
|
import {
|
||||||
|
Membership,
|
||||||
|
Key,
|
||||||
|
IUser,
|
||||||
|
User,
|
||||||
|
IServiceAccount,
|
||||||
|
ServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
MembershipNotFoundError,
|
MembershipNotFoundError,
|
||||||
BadRequestError
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
validateUserClientForWorkspace
|
||||||
|
} from '../helpers/user';
|
||||||
|
import {
|
||||||
|
validateServiceAccountClientForWorkspace
|
||||||
|
} from '../helpers/serviceAccount';
|
||||||
|
import {
|
||||||
|
validateServiceTokenDataClientForWorkspace
|
||||||
|
} from '../helpers/serviceTokenData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for membership with id [membershipId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
|
||||||
|
* @returns {Membership} - validated membership
|
||||||
|
*/
|
||||||
|
const validateClientForMembership = async ({
|
||||||
|
authData,
|
||||||
|
membershipId,
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
membershipId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const membership = await Membership.findById(membershipId);
|
||||||
|
|
||||||
|
if (!membership) throw MembershipNotFoundError({
|
||||||
|
message: 'Failed to find membership'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
await validateServiceTokenDataClientForWorkspace({
|
||||||
|
serviceTokenData: authData.authPayload,
|
||||||
|
workspaceId: new Types.ObjectId(membership.workspace)
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
@@ -21,7 +117,7 @@ const validateMembership = async ({
|
|||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId: Types.ObjectId;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
acceptedRoles?: string[];
|
acceptedRoles?: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
@@ -35,7 +131,7 @@ const validateMembership = async ({
|
|||||||
|
|
||||||
if (acceptedRoles) {
|
if (acceptedRoles) {
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
throw BadRequestError({ message: 'Failed to validate workspace membership role' });
|
throw BadRequestError({ message: 'Failed authorization for membership role' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +230,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForMembership,
|
||||||
validateMembership,
|
validateMembership,
|
||||||
addMemberships,
|
addMemberships,
|
||||||
findMembership,
|
findMembership,
|
||||||
|
|||||||
@@ -1,10 +1,98 @@
|
|||||||
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 {
|
import {
|
||||||
MembershipOrgNotFoundError,
|
MembershipOrgNotFoundError,
|
||||||
BadRequestError
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]
|
||||||
@@ -22,8 +110,8 @@ const validateMembershipOrg = async ({
|
|||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId: Types.ObjectId;
|
||||||
organizationId: Types.ObjectId;
|
organizationId: Types.ObjectId;
|
||||||
acceptedRoles: string[];
|
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
|
||||||
acceptedStatuses: string[];
|
acceptedStatuses?: Array<'invited' | 'accepted'>;
|
||||||
}) => {
|
}) => {
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: userId,
|
user: userId,
|
||||||
@@ -34,12 +122,16 @@ const validateMembershipOrg = async ({
|
|||||||
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
if (acceptedRoles) {
|
||||||
throw BadRequestError({ message: 'Failed to validate organization membership role' });
|
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||||
|
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
if (acceptedStatuses) {
|
||||||
throw BadRequestError({ message: 'Failed to validate organization membership status' });
|
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||||
|
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return membershipOrg;
|
return membershipOrg;
|
||||||
@@ -164,6 +256,7 @@ const deleteMembershipOrg = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForMembershipOrg,
|
||||||
validateMembershipOrg,
|
validateMembershipOrg,
|
||||||
findMembershipOrg,
|
findMembershipOrg,
|
||||||
addMembershipsOrg,
|
addMembershipsOrg,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY,
|
||||||
|
OWNER
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
import {
|
||||||
getStripeSecretKey,
|
getStripeSecretKey,
|
||||||
@@ -24,8 +25,15 @@ import {
|
|||||||
getStripeProductStarter
|
getStripeProductStarter
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
UnauthorizedRequestError
|
UnauthorizedRequestError,
|
||||||
|
OrganizationNotFoundError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
validateUserClientForOrganization
|
||||||
|
} from '../helpers/user';
|
||||||
|
import {
|
||||||
|
validateServiceAccountClientForOrganization
|
||||||
|
} from '../helpers/serviceAccount';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate accepted clients for organization with id [organizationId]
|
* Validate accepted clients for organization with id [organizationId]
|
||||||
@@ -35,34 +43,66 @@ import {
|
|||||||
*/
|
*/
|
||||||
const validateClientForOrganization = async ({
|
const validateClientForOrganization = async ({
|
||||||
authData,
|
authData,
|
||||||
organizationId
|
organizationId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
},
|
},
|
||||||
organizationId: string;
|
organizationId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
}) => {
|
}) => {
|
||||||
// TODO
|
|
||||||
|
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) {
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
// TODO
|
const membershipOrg = await validateUserClientForOrganization({
|
||||||
|
user: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ organization, membershipOrg });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
// TODO
|
await validateServiceAccountClientForOrganization({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
organization
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ organization });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
// TODO
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service token authorization for organization'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
// TODO
|
const membershipOrg = await validateUserClientForOrganization({
|
||||||
|
user: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ organization, membershipOrg });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: 'Failed client authorization for organization resource'
|
message: 'Failed client authorization for organization'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForOrganization,
|
||||||
createOrganization,
|
createOrganization,
|
||||||
initSubscriptionOrg,
|
initSubscriptionOrg,
|
||||||
updateSubscriptionOrgQuantity
|
updateSubscriptionOrgQuantity
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,15 +10,24 @@ import {
|
|||||||
ISecret
|
ISecret
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
|
validateMembership
|
||||||
|
} from '../helpers/membership';
|
||||||
|
import {
|
||||||
|
validateUserClientForSecret,
|
||||||
validateUserClientForSecrets
|
validateUserClientForSecrets
|
||||||
} from '../helpers/user';
|
} from '../helpers/user';
|
||||||
import {
|
import {
|
||||||
validateServiceTokenDataClientForSecrets
|
validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace
|
||||||
} from '../helpers/serviceTokenData';
|
} from '../helpers/serviceTokenData';
|
||||||
import {
|
import {
|
||||||
validateServiceAccountClientForSecrets
|
validateServiceAccountClientForSecrets,
|
||||||
|
validateServiceAccountClientForWorkspace
|
||||||
} from '../helpers/serviceAccount';
|
} from '../helpers/serviceAccount';
|
||||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
import {
|
||||||
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError,
|
||||||
|
SecretNotFoundError
|
||||||
|
} from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
@@ -27,12 +36,91 @@ import {
|
|||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate accepted clients for secrets with ids [secretIds]
|
* Validate authenticated clients for secrets with id [secretId] based
|
||||||
|
* on any known permissions.
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {User} obj.user - user client
|
* @param {Object} obj.authData - authenticated client details
|
||||||
* @param {ServiceAccount} obj.serviceAccount - service account client
|
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
|
||||||
* @param {ServiceTokenData} obj.service - service token client
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
* @param {String[]} obj.secretIds - ids of secrets to validate against
|
* @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 ({
|
const validateClientForSecrets = async ({
|
||||||
authData,
|
authData,
|
||||||
@@ -43,7 +131,7 @@ const validateClientForSecrets = async ({
|
|||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
},
|
},
|
||||||
secretIds: string[];
|
secretIds: Types.ObjectId[];
|
||||||
requiredPermissions: string[];
|
requiredPermissions: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
@@ -51,7 +139,7 @@ const validateClientForSecrets = async ({
|
|||||||
|
|
||||||
secrets = await Secret.find({
|
secrets = await Secret.find({
|
||||||
_id: {
|
_id: {
|
||||||
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
$in: secretIds
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,5 +193,6 @@ const validateClientForSecrets = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForSecret,
|
||||||
validateClientForSecrets
|
validateClientForSecrets
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
ISecret,
|
ISecret,
|
||||||
|
IOrganization,
|
||||||
|
IServiceAccountWorkspacePermission,
|
||||||
ServiceAccountWorkspacePermission
|
ServiceAccountWorkspacePermission
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
@@ -109,21 +111,20 @@ const validateClientForServiceAccount = async ({
|
|||||||
environment?: string;
|
environment?: string;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: add service account API support for workspace-level endpoints that are not
|
|
||||||
// tied to any specific environment
|
|
||||||
|
|
||||||
if (environment) {
|
if (environment) {
|
||||||
|
// case: environment specified ->
|
||||||
|
// evaluate service account authorization for workspace
|
||||||
|
// in the context of a specific environment [environment]
|
||||||
const permission = await ServiceAccountWorkspacePermission.findOne({
|
const permission = await ServiceAccountWorkspacePermission.findOne({
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!permission) throw UnauthorizedRequestError({
|
if (!permission) throw UnauthorizedRequestError({
|
||||||
message: 'Failed service account authorization for the given workspace environment'
|
message: 'Failed service account authorization for the given workspace environment'
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: refactor
|
|
||||||
let runningIsDisallowed = false;
|
let runningIsDisallowed = false;
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
switch (requiredPermission) {
|
switch (requiredPermission) {
|
||||||
@@ -143,6 +144,20 @@ const validateClientForServiceAccount = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} 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'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +195,6 @@ const validateClientForServiceAccount = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
// TODO: refactor
|
|
||||||
let runningIsDisallowed = false;
|
let runningIsDisallowed = false;
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
switch (requiredPermission) {
|
switch (requiredPermission) {
|
||||||
@@ -202,9 +216,6 @@ const validateClientForServiceAccount = async ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,9 +242,30 @@ const validateServiceAccountClientForServiceAccount = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export {
|
||||||
validateClientForServiceAccount,
|
validateClientForServiceAccount,
|
||||||
validateServiceAccountClientForWorkspace,
|
validateServiceAccountClientForWorkspace,
|
||||||
validateServiceAccountClientForSecrets,
|
validateServiceAccountClientForSecrets,
|
||||||
validateServiceAccountClientForServiceAccount
|
validateServiceAccountClientForServiceAccount,
|
||||||
|
validateServiceAccountClientForOrganization
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,94 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
ISecret,
|
ISecret,
|
||||||
IServiceTokenData
|
IServiceTokenData,
|
||||||
|
ServiceTokenData,
|
||||||
|
IUser,
|
||||||
|
User,
|
||||||
|
IServiceAccount,
|
||||||
|
ServiceAccount,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
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
|
* Validate that service token (client) can access workspace
|
||||||
@@ -34,20 +119,24 @@ import { UnauthorizedRequestError } from '../utils/errors';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceTokenData.environment !== environment) {
|
if (environment) {
|
||||||
// case: invalid environment passed
|
// case: environment is specified
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed service token authorization for the given workspace environment'
|
if (serviceTokenData.environment !== environment) {
|
||||||
});
|
// case: invalid environment passed
|
||||||
}
|
|
||||||
|
|
||||||
requiredPermissions?.forEach((permission) => {
|
|
||||||
if (!serviceTokenData.permissions.includes(permission)) {
|
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
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}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,6 +183,7 @@ import { UnauthorizedRequestError } from '../utils/errors';
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForServiceTokenData,
|
||||||
validateServiceTokenDataClientForWorkspace,
|
validateServiceTokenDataClientForWorkspace,
|
||||||
validateServiceTokenDataClientForSecrets
|
validateServiceTokenDataClientForSecrets
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
ISecret,
|
ISecret,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
User,
|
User,
|
||||||
Membership
|
Membership,
|
||||||
|
IOrganization,
|
||||||
|
Organization,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { sendMail } from './nodemailer';
|
import { sendMail } from './nodemailer';
|
||||||
import { validateMembership } from './membership';
|
import { validateMembership } from './membership';
|
||||||
@@ -177,21 +179,23 @@ const validateUserClientForWorkspace = async ({
|
|||||||
user,
|
user,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
}: {
|
}: {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// validate user membership in workspace
|
// validate user membership in workspace
|
||||||
const membership = await validateMembership({
|
const membership = await validateMembership({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: refactor
|
|
||||||
let runningIsDisallowed = false;
|
let runningIsDisallowed = false;
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
switch (requiredPermission) {
|
switch (requiredPermission) {
|
||||||
@@ -215,6 +219,42 @@ const validateUserClientForWorkspace = async ({
|
|||||||
return membership;
|
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]
|
* Validate that user (client) can access secrets [secrets]
|
||||||
* with required permissions [requiredPermissions]
|
* with required permissions [requiredPermissions]
|
||||||
@@ -232,7 +272,8 @@ const validateUserClientForWorkspace = async ({
|
|||||||
secrets: ISecret[];
|
secrets: ISecret[];
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: refactor
|
|
||||||
|
// TODO: add acceptedRoles?
|
||||||
|
|
||||||
const userMemberships = await Membership.find({ user: user._id })
|
const userMemberships = await Membership.find({ user: user._id })
|
||||||
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
||||||
@@ -288,11 +329,40 @@ const validateUserClientForServiceAccount = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export {
|
||||||
setupAccount,
|
setupAccount,
|
||||||
completeAccount,
|
completeAccount,
|
||||||
checkUserDevice,
|
checkUserDevice,
|
||||||
validateUserClientForWorkspace,
|
validateUserClientForWorkspace,
|
||||||
validateUserClientForSecrets,
|
validateUserClientForSecrets,
|
||||||
validateUserClientForServiceAccount
|
validateUserClientForServiceAccount,
|
||||||
|
validateUserClientForOrganization,
|
||||||
|
validateUserClientForSecret
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { validateUserClientForWorkspace } from '../helpers/user';
|
|||||||
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
||||||
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
|
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import { validateMembership } from '../helpers/membership';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
@@ -34,28 +34,38 @@ import {
|
|||||||
* @param {Object} obj.authData - authenticated client details
|
* @param {Object} obj.authData - authenticated client details
|
||||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||||
* @param {String} obj.environment - (optional) environment in 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
|
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateClientForWorkspace = async ({
|
const validateClientForWorkspace = async ({
|
||||||
authData,
|
authData,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
},
|
};
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
requiredPermissions?: string[];
|
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) {
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
const membership = await validateUserClientForWorkspace({
|
const membership = await validateUserClientForWorkspace({
|
||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,6 +99,7 @@ const validateClientForWorkspace = async ({
|
|||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,7 +107,7 @@ const validateClientForWorkspace = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: 'Failed client authorization for workspace resource'
|
message: 'Failed client authorization for workspace'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,18 +48,18 @@ import {
|
|||||||
integrationAuth as v1IntegrationAuthRouter
|
integrationAuth as v1IntegrationAuthRouter
|
||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v2SignupRouter,
|
signup as v2SignupRouter,
|
||||||
auth as v2AuthRouter,
|
auth as v2AuthRouter,
|
||||||
users as v2UsersRouter,
|
users as v2UsersRouter,
|
||||||
organizations as v2OrganizationsRouter,
|
organizations as v2OrganizationsRouter,
|
||||||
workspace as v2WorkspaceRouter,
|
workspace as v2WorkspaceRouter,
|
||||||
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,
|
serviceAccounts as v2ServiceAccountsRouter,
|
||||||
apiKeyData as v2APIKeyDataRouter,
|
apiKeyData as v2APIKeyDataRouter,
|
||||||
environment as v2EnvironmentRouter,
|
environment as v2EnvironmentRouter,
|
||||||
tags as v2TagsRouter,
|
tags as v2TagsRouter,
|
||||||
} from './routes/v2';
|
} from './routes/v2';
|
||||||
import { healthCheck } from './routes/status';
|
import { healthCheck } from './routes/status';
|
||||||
import { getLogger } from './utils/logger';
|
import { getLogger } from './utils/logger';
|
||||||
@@ -172,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 () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
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,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
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 {
|
||||||
@@ -116,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);
|
||||||
@@ -608,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 };
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
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,
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
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";
|
import axios from "axios";
|
||||||
@@ -157,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);
|
||||||
@@ -1618,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 };
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const requireAuth = ({
|
|||||||
acceptedAuthModes: string[];
|
acceptedAuthModes: string[];
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|
||||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||||
// and return token type [authTokenType] and value [authTokenValue]
|
// and return token type [authTokenType] and value [authTokenValue]
|
||||||
const { authMode, authTokenValue } = validateAuthMode({
|
const { authMode, authTokenValue } = validateAuthMode({
|
||||||
@@ -87,7 +88,7 @@ const requireAuth = ({
|
|||||||
|
|
||||||
req.authData = {
|
req.authData = {
|
||||||
authMode,
|
authMode,
|
||||||
authPayload
|
authPayload // User, ServiceAccount, ServiceTokenData
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -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,
|
|
||||||
workspaceId: bot.workspace,
|
|
||||||
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,
|
|
||||||
workspaceId: integration.workspace,
|
|
||||||
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,
|
|
||||||
workspaceId: integrationAuth.workspace._id,
|
|
||||||
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();
|
||||||
|
|||||||
@@ -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,
|
|
||||||
workspaceId: membership.workspace,
|
|
||||||
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 { validateMembershipOrg } from '../helpers/membershipOrg';
|
import {
|
||||||
|
validateClientForMembershipOrg,
|
||||||
|
validateMembershipOrg
|
||||||
|
} from '../helpers/membershipOrg';
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: transform
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,32 +24,23 @@ type req = 'params' | 'body' | 'query';
|
|||||||
const requireMembershipOrgAuth = ({
|
const requireMembershipOrgAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses,
|
acceptedStatuses,
|
||||||
location = 'params'
|
locationMembershipOrgId = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
acceptedStatuses: string[];
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
location?: req;
|
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,
|
||||||
req.targetMembership = await validateMembershipOrg({
|
acceptedStatuses
|
||||||
userId: req.user._id,
|
});
|
||||||
organizationId: membershipOrg.organization,
|
|
||||||
acceptedRoles,
|
return next();
|
||||||
acceptedStatuses
|
|
||||||
});
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
return next(UnauthorizedRequestError({
|
|
||||||
message: 'Unable to validate organization membership'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 { validateMembershipOrg } from '../helpers/membershipOrg';
|
||||||
|
import { validateClientForOrganization } from '../helpers/organization';
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
@@ -16,20 +17,29 @@ type req = 'params' | 'body' | 'query';
|
|||||||
const requireOrganizationAuth = ({
|
const requireOrganizationAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses,
|
acceptedStatuses,
|
||||||
location = 'params'
|
locationOrganizationId = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
acceptedStatuses: string[];
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
location?: req;
|
locationOrganizationId?: req;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const { organizationId } = req[location];
|
const { organizationId } = req[locationOrganizationId];
|
||||||
req.membershipOrg = await validateMembershipOrg({
|
|
||||||
userId: req.user._id,
|
const { organization, membershipOrg } = await validateClientForOrganization({
|
||||||
|
authData: req.authData,
|
||||||
organizationId: new Types.ObjectId(organizationId),
|
organizationId: new Types.ObjectId(organizationId),
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses
|
acceptedStatuses
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
req.organization = organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membershipOrg) {
|
||||||
|
req.membershipOrg = membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
workspaceId: secret.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
req._secret = secret;
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
|
||||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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 { validateClientForSecrets } from '../helpers/secrets';
|
import { validateClientForSecrets } from '../helpers/secrets';
|
||||||
@@ -24,7 +25,7 @@ const requireSecretsAuth = ({
|
|||||||
|
|
||||||
req.secrets = await validateClientForSecrets({
|
req.secrets = await validateClientForSecrets({
|
||||||
authData: req.authData,
|
authData: req.authData,
|
||||||
secretIds: [req.body.secretIds],
|
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const requireServiceAccountWorkspacePermissionAuth = ({
|
|||||||
acceptedStatuses,
|
acceptedStatuses,
|
||||||
location = 'params'
|
location = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
acceptedStatuses: string[];
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
location?: req;
|
location?: req;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
workspaceId: serviceTokenData.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
req.serviceTokenData = serviceTokenData;
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ const requireWorkspaceAuth = ({
|
|||||||
locationEnvironment = undefined,
|
locationEnvironment = undefined,
|
||||||
requiredPermissions = []
|
requiredPermissions = []
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
locationWorkspaceId: req;
|
locationWorkspaceId: req;
|
||||||
locationEnvironment?: req | undefined;
|
locationEnvironment?: req | undefined;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|
||||||
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
||||||
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
|
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
|
||||||
|
|
||||||
@@ -34,6 +33,7 @@ const requireWorkspaceAuth = ({
|
|||||||
authData: req.authData,
|
authData: req.authData,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
|
||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
@@ -42,7 +43,8 @@ export interface IIntegration {
|
|||||||
| 'railway'
|
| 'railway'
|
||||||
| 'flyio'
|
| 'flyio'
|
||||||
| 'circleci'
|
| 'circleci'
|
||||||
| 'travisci';
|
| 'travisci'
|
||||||
|
| 'supabase';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -13,12 +13,13 @@ import {
|
|||||||
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' | 'railway' | '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;
|
||||||
@@ -56,6 +57,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
ADMIN,
|
ADMIN,
|
||||||
MEMBER,
|
MEMBER,
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_TOKEN
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
PERMISSION_READ_SECRETS,
|
||||||
|
PERMISSION_WRITE_SECRETS
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||||
import { secretController } from '../../controllers/v2';
|
import { secretController } from '../../controllers/v2';
|
||||||
@@ -75,7 +77,8 @@ router.get(
|
|||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
|
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
|
||||||
@@ -103,7 +106,8 @@ router.delete(
|
|||||||
acceptedAuthModes: [AUTH_MODE_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,
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -47,7 +48,7 @@ router.post(
|
|||||||
if (secretIds.length > 0) {
|
if (secretIds.length > 0) {
|
||||||
req.secrets = await validateClientForSecrets({
|
req.secrets = await validateClientForSecrets({
|
||||||
authData: req.authData,
|
authData: req.authData,
|
||||||
secretIds,
|
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
|
||||||
requiredPermissions: []
|
requiredPermissions: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ router.post(
|
|||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
acceptedStatuses: [ACCEPTED],
|
acceptedStatuses: [ACCEPTED],
|
||||||
location: 'body'
|
locationOrganizationId: 'body'
|
||||||
}),
|
}),
|
||||||
serviceAccountsController.createServiceAccount
|
serviceAccountsController.createServiceAccount
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ router.patch( // TODO - rewire dashboard to this route
|
|||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: 'params'
|
||||||
}),
|
}),
|
||||||
requireMembershipAuth({
|
requireMembershipAuth({
|
||||||
acceptedRoles: [ADMIN]
|
acceptedRoles: [ADMIN],
|
||||||
|
locationMembershipId: 'params'
|
||||||
}),
|
}),
|
||||||
workspaceController.updateWorkspaceMembership
|
workspaceController.updateWorkspaceMembership
|
||||||
);
|
);
|
||||||
@@ -124,7 +125,8 @@ router.delete( // TODO - rewire dashboard to this route
|
|||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: 'params'
|
||||||
}),
|
}),
|
||||||
requireMembershipAuth({
|
requireMembershipAuth({
|
||||||
acceptedRoles: [ADMIN]
|
acceptedRoles: [ADMIN],
|
||||||
|
locationMembershipId: 'params'
|
||||||
}),
|
}),
|
||||||
workspaceController.deleteWorkspaceMembership
|
workspaceController.deleteWorkspaceMembership
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
handleOAuthExchangeHelper,
|
handleOAuthExchangeHelper,
|
||||||
syncIntegrationsHelper,
|
syncIntegrationsHelper,
|
||||||
@@ -67,7 +68,7 @@ class IntegrationService {
|
|||||||
* @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
|
||||||
*/
|
*/
|
||||||
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
|
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: Types.ObjectId}) {
|
||||||
return await getIntegrationAuthRefreshHelper({
|
return await getIntegrationAuthRefreshHelper({
|
||||||
integrationAuthId
|
integrationAuthId
|
||||||
});
|
});
|
||||||
@@ -80,7 +81,7 @@ class IntegrationService {
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @param {String} accessToken - decrypted access token
|
* @param {String} accessToken - decrypted access token
|
||||||
*/
|
*/
|
||||||
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
|
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) {
|
||||||
return await getIntegrationAuthAccessHelper({
|
return await getIntegrationAuthAccessHelper({
|
||||||
integrationAuthId
|
integrationAuthId
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
} from '../config';
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
|
User,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
|
ServiceAccount,
|
||||||
IServiceTokenData
|
IServiceTokenData
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
@@ -56,7 +58,7 @@ class Telemetry {
|
|||||||
}: {
|
}: {
|
||||||
user?: IUser;
|
user?: IUser;
|
||||||
serviceAccount?: IServiceAccount;
|
serviceAccount?: IServiceAccount;
|
||||||
serviceTokenData?: IServiceTokenData;
|
serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated)
|
||||||
}) => {
|
}) => {
|
||||||
let distinctId = '';
|
let distinctId = '';
|
||||||
|
|
||||||
@@ -65,11 +67,13 @@ class Telemetry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (serviceAccount) {
|
if (serviceAccount) {
|
||||||
distinctId = `sa.${serviceAccount._id}`;
|
distinctId = `sa.${serviceAccount._id.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceTokenData) {
|
if (serviceTokenData?.user && serviceTokenData?.user instanceof User) {
|
||||||
distinctId = `st.${serviceTokenData._id}`;
|
distinctId = serviceTokenData.user.email;
|
||||||
|
} else if (serviceTokenData?.serviceAccount && serviceTokenData?.serviceAccount instanceof ServiceAccount) {
|
||||||
|
distinctId = `sa.${serviceTokenData.serviceAccount._id.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distinctId === '') {
|
if (distinctId === '') {
|
||||||
|
|||||||
@@ -8,17 +8,18 @@ import { Key, Membership, MembershipOrg, Organization, User, Workspace } from ".
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { getNodeEnv } from '../config';
|
import { getNodeEnv } from '../config';
|
||||||
|
|
||||||
export const createTestUserForDevelopment = async () => {
|
export const testUserEmail = "[email protected]"
|
||||||
if (getNodeEnv() === "development") {
|
export const testUserPassword = "testInfisical1"
|
||||||
const testUserEmail = "[email protected]"
|
export const testUserId = "63cefa6ec8d3175601cfa980"
|
||||||
const testUserPassword = "testInfisical1"
|
export const testWorkspaceId = "63cefb15c8d3175601cfa989"
|
||||||
const testUserId = "63cefa6ec8d3175601cfa980"
|
export const testOrgId = "63cefb15c8d3175601cfa985"
|
||||||
const testWorkspaceId = "63cefb15c8d3175601cfa989"
|
export const testMembershipId = "63cefb159185d9aa3ef0cf35"
|
||||||
const testOrgId = "63cefb15c8d3175601cfa985"
|
export const testMembershipOrgId = "63cefb159185d9aa3ef0cf31"
|
||||||
const testMembershipId = "63cefb159185d9aa3ef0cf35"
|
export const testWorkspaceKeyId = "63cf48f0225e6955acec5eff"
|
||||||
const testMembershipOrgId = "63cefb159185d9aa3ef0cf31"
|
export const plainTextWorkspaceKey = "543fef8224813a46230b0a50a46c5fb2"
|
||||||
const testWorkspaceKeyId = "63cf48f0225e6955acec5eff"
|
|
||||||
|
|
||||||
|
export const createTestUserForDevelopment = async () => {
|
||||||
|
if (getNodeEnv() === "development" || getNodeEnv() === "test") {
|
||||||
const testUser = {
|
const testUser = {
|
||||||
_id: testUserId,
|
_id: testUserId,
|
||||||
email: testUserEmail,
|
email: testUserEmail,
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ export const ValidationError = (error?: Partial<RequestErrorContext>) => new Req
|
|||||||
stack: error?.stack
|
stack: error?.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//* ----->[INTEGRATION AUTH ERRORS]<-----
|
||||||
|
export const IntegrationAuthNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
statusCode: error?.statusCode ?? 404,
|
||||||
|
type: error?.type ?? 'integration_auth_not_found_error',
|
||||||
|
message: error?.message ?? 'The requested integration authorization was not found',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
|
||||||
//* ----->[INTEGRATION ERRORS]<-----
|
//* ----->[INTEGRATION ERRORS]<-----
|
||||||
export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
@@ -202,4 +212,13 @@ export const ServiceAccountKeyNotFoundError = (error?: Partial<RequestErrorConte
|
|||||||
stack: error?.stack
|
stack: error?.stack
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const BotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
statusCode: error?.statusCode ?? 404,
|
||||||
|
type: error?.type ?? 'bot_not_found_error',
|
||||||
|
message: error?.message ?? 'The requested bot was not found',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
//* ----->[MISC ERRORS]<-----
|
//* ----->[MISC ERRORS]<-----
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
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,
|
||||||
getIntegrationOptions
|
getIntegrationOptions
|
||||||
} from "./integration";
|
} from "./integration";
|
||||||
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
|
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
|
||||||
@@ -102,6 +104,7 @@ export {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -119,6 +122,7 @@ export {
|
|||||||
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,
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const INTEGRATION_RAILWAY = "railway";
|
|||||||
const INTEGRATION_FLYIO = "flyio";
|
const INTEGRATION_FLYIO = "flyio";
|
||||||
const INTEGRATION_CIRCLECI = "circleci";
|
const INTEGRATION_CIRCLECI = "circleci";
|
||||||
const INTEGRATION_TRAVISCI = "travisci";
|
const INTEGRATION_TRAVISCI = "travisci";
|
||||||
|
const INTEGRATION_SUPABASE = 'supabase';
|
||||||
const INTEGRATION_SET = new Set([
|
const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
@@ -32,6 +33,7 @@ const INTEGRATION_SET = new Set([
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// integration types
|
// integration types
|
||||||
@@ -57,6 +59,7 @@ const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
|
|||||||
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||||
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||||
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
||||||
|
const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
|
||||||
|
|
||||||
const getIntegrationOptions = () => {
|
const getIntegrationOptions = () => {
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
@@ -178,6 +181,15 @@ const getIntegrationOptions = () => {
|
|||||||
clientId: '',
|
clientId: '',
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Supabase',
|
||||||
|
slug: 'supabase',
|
||||||
|
image: 'Supabase.png',
|
||||||
|
isAvailable: true,
|
||||||
|
type: 'pat',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Google Cloud Platform',
|
name: 'Google Cloud Platform',
|
||||||
slug: 'gcp',
|
slug: 'gcp',
|
||||||
@@ -207,6 +219,7 @@ export {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -224,5 +237,6 @@ export {
|
|||||||
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,
|
||||||
getIntegrationOptions
|
getIntegrationOptions
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,3 +3,8 @@ process.env.MONGO_URL =
|
|||||||
'mongodb://test:test1234@localhost:27018/?authSource=admin';
|
'mongodb://test:test1234@localhost:27018/?authSource=admin';
|
||||||
process.env.MONGO_USERNAME = 'test';
|
process.env.MONGO_USERNAME = 'test';
|
||||||
process.env.MONGO_PASSWORD = 'test1234';
|
process.env.MONGO_PASSWORD = 'test1234';
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392"
|
||||||
|
process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364"
|
||||||
|
process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2"
|
||||||
|
process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyCiphertext": "eaX9a2g=",
|
||||||
|
"secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==",
|
||||||
|
"secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==",
|
||||||
|
"secretValueCiphertext": "cw==",
|
||||||
|
"secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==",
|
||||||
|
"secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==",
|
||||||
|
"secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==",
|
||||||
|
"secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==",
|
||||||
|
"secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==",
|
||||||
|
"secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==",
|
||||||
|
"secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==",
|
||||||
|
"secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==",
|
||||||
|
"secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==",
|
||||||
|
"secretValueCiphertext": "cw==",
|
||||||
|
"secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==",
|
||||||
|
"secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyCiphertext": "eaX9a2g=",
|
||||||
|
"secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==",
|
||||||
|
"secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==",
|
||||||
|
"secretValueCiphertext": "cw==",
|
||||||
|
"secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==",
|
||||||
|
"secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==",
|
||||||
|
"secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyCiphertext": "eaX9a2g=",
|
||||||
|
"secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==",
|
||||||
|
"secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==",
|
||||||
|
"secretValueCiphertext": "cw==",
|
||||||
|
"secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==",
|
||||||
|
"secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==",
|
||||||
|
"secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyCiphertext": "eaX9a2g=",
|
||||||
|
"secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==",
|
||||||
|
"secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==",
|
||||||
|
"secretValueCiphertext": "cw==",
|
||||||
|
"secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==",
|
||||||
|
"secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==",
|
||||||
|
"secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "shared",
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyCiphertext": "IVMtGWE=",
|
||||||
|
"secretKeyIV": "BDsG7/ylk7mT8MrIMn0e7w==",
|
||||||
|
"secretKeyTag": "1ujy08fctmZ1xTXMYr23UQ==",
|
||||||
|
"secretValueCiphertext": "I9psUg==",
|
||||||
|
"secretValueIV": "W+DJETpCerHkFv8AR9Fv4w==",
|
||||||
|
"secretValueTag": "yODOeN3HBr/usly4VSMt9w==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "QET7oX2ZiuLDSzwrkeL2Ig==",
|
||||||
|
"secretCommentTag": "6P3xeA9eO+3Wp66ROHXgfg=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"secret": {
|
||||||
|
"workspace": "63cefb15c8d3175601cfa989",
|
||||||
|
"type": "personal",
|
||||||
|
"user": "63cefa6ec8d3175601cfa980",
|
||||||
|
"tags": [],
|
||||||
|
"environment": "dev",
|
||||||
|
"secretKeyCiphertext": "Q7lyRO8=",
|
||||||
|
"secretKeyIV": "yz8koc3d63ywJMiGXpCNSw==",
|
||||||
|
"secretKeyTag": "j2bMQ2d4sDZKA0OaKM5SXA==",
|
||||||
|
"secretValueCiphertext": "X4kaiShmtGZt",
|
||||||
|
"secretValueIV": "p/GdbksLVveNLsV3vz5GLA==",
|
||||||
|
"secretValueTag": "//dhRL+pagecavHJCtMPWg==",
|
||||||
|
"secretCommentCiphertext": "",
|
||||||
|
"secretCommentIV": "7eYJzuilvjQPutqrqbd2MQ==",
|
||||||
|
"secretCommentTag": "LpPv9K0Hhd5noE39Zu9U+w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Helper functions for integration tests
|
||||||
|
|
||||||
|
import axiosInstance from "../../src/config/request";
|
||||||
|
import { Secret } from "../../src/models";
|
||||||
|
import { testUserEmail, testUserPassword } from "../../src/utils/addDevelopmentUser";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const crypto = require('crypto')
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const jsrp = require('jsrp');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const axios = require('axios');
|
||||||
|
import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser";
|
||||||
|
import { encryptSymmetric } from "../../src/utils/crypto";
|
||||||
|
|
||||||
|
interface TokenData {
|
||||||
|
token: string;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getJWTFromTestUser = (): Promise<TokenData> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = new jsrp.client();
|
||||||
|
const EMAIL = testUserEmail
|
||||||
|
const PASSWORD = testUserPassword
|
||||||
|
|
||||||
|
client.init({
|
||||||
|
username: EMAIL,
|
||||||
|
password: PASSWORD,
|
||||||
|
}, async () => {
|
||||||
|
const clientPublicKey = client.getPublicKey();
|
||||||
|
|
||||||
|
// POST: /login1
|
||||||
|
const reqBody = {
|
||||||
|
email: EMAIL,
|
||||||
|
clientPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const loginOneRes = await axiosInstance.post('http://localhost:4000/api/v1/auth/login1', reqBody);
|
||||||
|
const serverPublicKey = loginOneRes.data.serverPublicKey;
|
||||||
|
const salt = loginOneRes.data.salt;
|
||||||
|
|
||||||
|
client.setSalt(salt);
|
||||||
|
client.setServerPublicKey(serverPublicKey);
|
||||||
|
const clientSharedKey = client.getSharedKey(); // shared Key
|
||||||
|
const clientProof = client.getProof(); // called M1
|
||||||
|
|
||||||
|
// POST: /login2
|
||||||
|
const reqBody2 = {
|
||||||
|
email: EMAIL,
|
||||||
|
clientProof
|
||||||
|
}
|
||||||
|
|
||||||
|
const response2 = await axiosInstance.post('http://localhost:4000/api/v1/auth/login2', reqBody2);
|
||||||
|
|
||||||
|
resolve(response2.data)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServiceTokenFromTestUser = async () => {
|
||||||
|
const loggedInUserDetails = await getJWTFromTestUser()
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: plainTextWorkspaceKey,
|
||||||
|
key: randomBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newServiceToken = await axiosInstance.post('http://localhost:4000/api/v2/service-token/', {
|
||||||
|
'name': "test service token",
|
||||||
|
'workspaceId': testWorkspaceId,
|
||||||
|
'environment': "dev",
|
||||||
|
'encryptedKey': ciphertext,
|
||||||
|
'iv': iv,
|
||||||
|
'tag': tag,
|
||||||
|
'expiresIn': Date.now() + 90000,
|
||||||
|
'permissions': ["read"]
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${loggedInUserDetails.token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${newServiceToken.data.serviceToken}.${randomBytes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAllSecrets = async () => {
|
||||||
|
await Secret.deleteMany()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllSecrets = async () => {
|
||||||
|
return await Secret.find()
|
||||||
|
}
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
import request from 'supertest'
|
||||||
|
import main from '../../../../src/index'
|
||||||
|
import { testWorkspaceId } from '../../../../src/utils/addDevelopmentUser';
|
||||||
|
import { deleteAllSecrets, getAllSecrets, getJWTFromTestUser, getServiceTokenFromTestUser } from '../../../helper/helper';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const batchSecretRequestWithNoOverride = require('../../../data/batch-secrets-no-override.json');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const batchSecretRequestWithOverrides = require('../../../data/batch-secrets-with-overrides.json');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const batchSecretRequestWithBadRequest = require('../../../data/batch-create-secrets-with-some-missing-params.json');
|
||||||
|
|
||||||
|
let server: any;
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = await main;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /api/v2/secrets", () => {
|
||||||
|
describe("Get secrets via JTW", () => {
|
||||||
|
test("should create secrets and read secrets via jwt", async () => {
|
||||||
|
try {
|
||||||
|
// get login details
|
||||||
|
const loginResponse = await getJWTFromTestUser()
|
||||||
|
|
||||||
|
// create creates
|
||||||
|
const createSecretsResponse = await request(server)
|
||||||
|
.post("/api/v2/secrets/batch")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.send({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev",
|
||||||
|
requests: batchSecretRequestWithNoOverride
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createSecretsResponse.statusCode).toBe(200)
|
||||||
|
|
||||||
|
|
||||||
|
const getSecrets = await request(server)
|
||||||
|
.get("/api/v2/secrets")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.query({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getSecrets.statusCode).toBe(200)
|
||||||
|
expect(getSecrets.body).toHaveProperty("secrets")
|
||||||
|
expect(getSecrets.body.secrets).toHaveLength(3)
|
||||||
|
expect(getSecrets.body.secrets).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
getSecrets.body.secrets.forEach((secret: any) => {
|
||||||
|
expect(secret).toHaveProperty('_id');
|
||||||
|
expect(secret._id).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('version');
|
||||||
|
expect(secret.version).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('workspace');
|
||||||
|
expect(secret.workspace).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('type');
|
||||||
|
expect(secret.type).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('tags');
|
||||||
|
expect(secret.tags).toHaveLength(0);
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('environment');
|
||||||
|
expect(secret.environment).toEqual("dev");
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyCiphertext');
|
||||||
|
expect(secret.secretKeyCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyIV');
|
||||||
|
expect(secret.secretKeyIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyTag');
|
||||||
|
expect(secret.secretKeyTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueCiphertext');
|
||||||
|
expect(secret.secretValueCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueIV');
|
||||||
|
expect(secret.secretValueIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueTag');
|
||||||
|
expect(secret.secretValueTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentCiphertext');
|
||||||
|
expect(secret.secretCommentCiphertext).toBeFalsy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentIV');
|
||||||
|
expect(secret.secretCommentIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentTag');
|
||||||
|
expect(secret.secretCommentTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('createdAt');
|
||||||
|
expect(secret.createdAt).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('updatedAt');
|
||||||
|
expect(secret.updatedAt).toBeTruthy();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// clean up
|
||||||
|
await deleteAllSecrets()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Get secrets via jwt when personal overrides exist", async () => {
|
||||||
|
try {
|
||||||
|
// get login details
|
||||||
|
const loginResponse = await getJWTFromTestUser()
|
||||||
|
|
||||||
|
// create creates
|
||||||
|
const createSecretsResponse = await request(server)
|
||||||
|
.post("/api/v2/secrets/batch")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.send({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev",
|
||||||
|
requests: batchSecretRequestWithOverrides
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createSecretsResponse.statusCode).toBe(200)
|
||||||
|
|
||||||
|
const getSecrets = await request(server)
|
||||||
|
.get("/api/v2/secrets")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.query({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getSecrets.statusCode).toBe(200)
|
||||||
|
expect(getSecrets.body).toHaveProperty("secrets")
|
||||||
|
expect(getSecrets.body.secrets).toHaveLength(2)
|
||||||
|
expect(getSecrets.body.secrets).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
getSecrets.body.secrets.forEach((secret: any) => {
|
||||||
|
expect(secret).toHaveProperty('_id');
|
||||||
|
expect(secret._id).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('version');
|
||||||
|
expect(secret.version).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('workspace');
|
||||||
|
expect(secret.workspace).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('type');
|
||||||
|
expect(secret.type).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('tags');
|
||||||
|
expect(secret.tags).toHaveLength(0);
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('environment');
|
||||||
|
expect(secret.environment).toEqual("dev");
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyCiphertext');
|
||||||
|
expect(secret.secretKeyCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyIV');
|
||||||
|
expect(secret.secretKeyIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyTag');
|
||||||
|
expect(secret.secretKeyTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueCiphertext');
|
||||||
|
expect(secret.secretValueCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueIV');
|
||||||
|
expect(secret.secretValueIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueTag');
|
||||||
|
expect(secret.secretValueTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentCiphertext');
|
||||||
|
expect(secret.secretCommentCiphertext).toBeFalsy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentIV');
|
||||||
|
expect(secret.secretCommentIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentTag');
|
||||||
|
expect(secret.secretCommentTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('createdAt');
|
||||||
|
expect(secret.createdAt).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('updatedAt');
|
||||||
|
expect(secret.updatedAt).toBeTruthy();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// clean up
|
||||||
|
await deleteAllSecrets()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("fetch secrets via service token", () => {
|
||||||
|
test("Get secrets via jwt when personal overrides exist", async () => {
|
||||||
|
try {
|
||||||
|
// get login details
|
||||||
|
const loginResponse = await getJWTFromTestUser()
|
||||||
|
|
||||||
|
// create creates
|
||||||
|
const createSecretsResponse = await request(server)
|
||||||
|
.post("/api/v2/secrets/batch")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.send({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev",
|
||||||
|
requests: batchSecretRequestWithOverrides
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createSecretsResponse.statusCode).toBe(200)
|
||||||
|
|
||||||
|
// now use the service token to fetch secrets
|
||||||
|
const serviceToken = await getServiceTokenFromTestUser()
|
||||||
|
|
||||||
|
const getSecrets = await request(server)
|
||||||
|
.get("/api/v2/secrets")
|
||||||
|
.set('Authorization', `Bearer ${serviceToken}`)
|
||||||
|
.query({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getSecrets.statusCode).toBe(200)
|
||||||
|
expect(getSecrets.body).toHaveProperty("secrets")
|
||||||
|
expect(getSecrets.body.secrets).toHaveLength(2)
|
||||||
|
expect(getSecrets.body.secrets).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
getSecrets.body.secrets.forEach((secret: any) => {
|
||||||
|
expect(secret).toHaveProperty('_id');
|
||||||
|
expect(secret._id).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('version');
|
||||||
|
expect(secret.version).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('workspace');
|
||||||
|
expect(secret.workspace).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('type');
|
||||||
|
expect(secret.type).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('tags');
|
||||||
|
expect(secret.tags).toHaveLength(0);
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('environment');
|
||||||
|
expect(secret.environment).toEqual("dev");
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyCiphertext');
|
||||||
|
expect(secret.secretKeyCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyIV');
|
||||||
|
expect(secret.secretKeyIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyTag');
|
||||||
|
expect(secret.secretKeyTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueCiphertext');
|
||||||
|
expect(secret.secretValueCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueIV');
|
||||||
|
expect(secret.secretValueIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueTag');
|
||||||
|
expect(secret.secretValueTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentCiphertext');
|
||||||
|
expect(secret.secretCommentCiphertext).toBeFalsy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentIV');
|
||||||
|
expect(secret.secretCommentIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentTag');
|
||||||
|
expect(secret.secretCommentTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('createdAt');
|
||||||
|
expect(secret.createdAt).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('updatedAt');
|
||||||
|
expect(secret.updatedAt).toBeTruthy();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// clean up
|
||||||
|
await deleteAllSecrets()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should create secrets and read secrets via service token when no overrides", async () => {
|
||||||
|
try {
|
||||||
|
// get login details
|
||||||
|
const loginResponse = await getJWTFromTestUser()
|
||||||
|
|
||||||
|
// create secrets
|
||||||
|
const createSecretsResponse = await request(server)
|
||||||
|
.post("/api/v2/secrets/batch")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.send({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev",
|
||||||
|
requests: batchSecretRequestWithNoOverride
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createSecretsResponse.statusCode).toBe(200)
|
||||||
|
|
||||||
|
|
||||||
|
// now use the service token to fetch secrets
|
||||||
|
const serviceToken = await getServiceTokenFromTestUser()
|
||||||
|
|
||||||
|
const getSecrets = await request(server)
|
||||||
|
.get("/api/v2/secrets")
|
||||||
|
.set('Authorization', `Bearer ${serviceToken}`)
|
||||||
|
.query({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getSecrets.statusCode).toBe(200)
|
||||||
|
expect(getSecrets.body).toHaveProperty("secrets")
|
||||||
|
expect(getSecrets.body.secrets).toHaveLength(3)
|
||||||
|
expect(getSecrets.body.secrets).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
getSecrets.body.secrets.forEach((secret: any) => {
|
||||||
|
expect(secret).toHaveProperty('_id');
|
||||||
|
expect(secret._id).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('version');
|
||||||
|
expect(secret.version).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('workspace');
|
||||||
|
expect(secret.workspace).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('type');
|
||||||
|
expect(secret.type).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('tags');
|
||||||
|
expect(secret.tags).toHaveLength(0);
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('environment');
|
||||||
|
expect(secret.environment).toEqual("dev");
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyCiphertext');
|
||||||
|
expect(secret.secretKeyCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyIV');
|
||||||
|
expect(secret.secretKeyIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretKeyTag');
|
||||||
|
expect(secret.secretKeyTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueCiphertext');
|
||||||
|
expect(secret.secretValueCiphertext).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueIV');
|
||||||
|
expect(secret.secretValueIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretValueTag');
|
||||||
|
expect(secret.secretValueTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentCiphertext');
|
||||||
|
expect(secret.secretCommentCiphertext).toBeFalsy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentIV');
|
||||||
|
expect(secret.secretCommentIV).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('secretCommentTag');
|
||||||
|
expect(secret.secretCommentTag).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('createdAt');
|
||||||
|
expect(secret.createdAt).toBeTruthy();
|
||||||
|
|
||||||
|
expect(secret).toHaveProperty('updatedAt');
|
||||||
|
expect(secret.updatedAt).toBeTruthy();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// clean up
|
||||||
|
await deleteAllSecrets()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("create secrets via JWT", () => {
|
||||||
|
test("Create secrets via jwt when some requests have missing required parameters", async () => {
|
||||||
|
// get login details
|
||||||
|
const loginResponse = await getJWTFromTestUser()
|
||||||
|
|
||||||
|
// create creates
|
||||||
|
const createSecretsResponse = await request(server)
|
||||||
|
.post("/api/v2/secrets/batch")
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.token}`)
|
||||||
|
.send({
|
||||||
|
workspaceId: testWorkspaceId,
|
||||||
|
environment: "dev",
|
||||||
|
requests: batchSecretRequestWithBadRequest
|
||||||
|
})
|
||||||
|
|
||||||
|
const allSecretsInDB = await getAllSecrets()
|
||||||
|
|
||||||
|
expect(createSecretsResponse.statusCode).toBe(500) // TODO should be set to 400
|
||||||
|
expect(allSecretsInDB).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import request from 'supertest'
|
||||||
|
import main from '../../../../src/index'
|
||||||
|
import { getServiceTokenFromTestUser } from '../../../helper/helper';
|
||||||
|
let server: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = await main;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /api/v2/service-token", () => {
|
||||||
|
describe("Get service token details", () => {
|
||||||
|
test("should respond create and get the details of a service token", async () => {
|
||||||
|
// generate a service token
|
||||||
|
const serviceToken = await getServiceTokenFromTestUser()
|
||||||
|
|
||||||
|
// get the service token details
|
||||||
|
const serviceTokenDetails = await request(server)
|
||||||
|
.get("/api/v2/service-token")
|
||||||
|
.set('Authorization', `Bearer ${serviceToken}`)
|
||||||
|
|
||||||
|
expect(serviceTokenDetails.body).toMatchObject({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: 'test service token',
|
||||||
|
workspace: '63cefb15c8d3175601cfa989',
|
||||||
|
environment: 'dev',
|
||||||
|
user: {
|
||||||
|
_id: '63cefa6ec8d3175601cfa980',
|
||||||
|
email: '[email protected]',
|
||||||
|
firstName: 'Jake',
|
||||||
|
lastName: 'Moni',
|
||||||
|
isMfaEnabled: false,
|
||||||
|
mfaMethods: expect.any(Array),
|
||||||
|
devices: [
|
||||||
|
{
|
||||||
|
ip: expect.any(String),
|
||||||
|
userAgent: expect.any(String),
|
||||||
|
_id: expect.any(String),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
},
|
||||||
|
lastUsed: expect.any(String),
|
||||||
|
expiresAt: expect.any(String),
|
||||||
|
encryptedKey: expect.any(String),
|
||||||
|
iv: expect.any(String),
|
||||||
|
tag: expect.any(String),
|
||||||
|
permissions: ['read'],
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric
|
encryptSymmetric
|
||||||
} from '../../src/utils/crypto';
|
} from '../../../src/utils/crypto';
|
||||||
|
|
||||||
describe('Crypto', () => {
|
describe('Crypto', () => {
|
||||||
describe('encryptAsymmetric', () => {
|
describe('encryptAsymmetric', () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from '@jest/globals';
|
import { describe, test, expect } from '@jest/globals';
|
||||||
import { getChannelFromUserAgent } from '../../src/utils/posthog';
|
import { getChannelFromUserAgent } from '../../../src/utils/posthog';
|
||||||
|
|
||||||
describe('posthog getChannelFromUserAgent', () => {
|
describe('posthog getChannelFromUserAgent', () => {
|
||||||
test("should return 'web' when userAgent includes 'mozilla'", () => {
|
test("should return 'web' when userAgent includes 'mozilla'", () => {
|
||||||
@@ -230,19 +230,10 @@ type GetEncryptedSecretsV2Response struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetServiceTokenDetailsResponse struct {
|
type GetServiceTokenDetailsResponse struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Workspace string `json:"workspace"`
|
Workspace string `json:"workspace"`
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
User struct {
|
|
||||||
ID string `json:"_id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
|
||||||
V int `json:"__v"`
|
|
||||||
FirstName string `json:"firstName"`
|
|
||||||
LastName string `json:"lastName"`
|
|
||||||
} `json:"user"`
|
|
||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
EncryptedKey string `json:"encryptedKey"`
|
EncryptedKey string `json:"encryptedKey"`
|
||||||
Iv string `json:"iv"`
|
Iv string `json:"iv"`
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ Resources:
|
|||||||
echo "JWT_AUTH_SECRET=${!JWT_AUTH_SECRET}" >> .env
|
echo "JWT_AUTH_SECRET=${!JWT_AUTH_SECRET}" >> .env
|
||||||
echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env
|
echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env
|
||||||
echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env
|
echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env
|
||||||
|
echo "HTTPS_ENABLED=false" >> .env
|
||||||
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
---
|
---
|
||||||
title: "Authentication"
|
title: "Authentication"
|
||||||
|
description: "How to authenticate with the Infisical Public API"
|
||||||
---
|
---
|
||||||
|
|
||||||
To authenticate requests with Infisical, you can either use an API Key or [Infisical Token](../../../getting-started/dashboard/token); certain endpoints will accept either one or both.
|
## Essentials
|
||||||
- API Key: This general-purpose authentication token provides user access to most endpoints in this reference.
|
|
||||||
- [Infisical Token](../../../getting-started/dashboard/token): This authentication token (also referred to as the service token) is scoped to a specific project and environment and used for CRUD secret operations.
|
The Public API accepts multiple modes of authentication being via API Key, Service Account credentials, or [Infisical Token](../../../getting-started/dashboard/token).
|
||||||
|
|
||||||
|
- API Key: Provides full access to all endpoints representing the user.
|
||||||
|
- [Service Account](): Provides scoped access to an organization and select projects representing a machine such as a VM or application client.
|
||||||
|
- [Infisical Token](../../../getting-started/dashboard/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment.
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="API Key">
|
<Accordion title="API Key">
|
||||||
|
The API key mode uses an API key to authenticate with the API.
|
||||||
|
|
||||||
To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform.
|
To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform.
|
||||||
|
|
||||||
You can obtain an API key in User Settings > API Keys
|
You can obtain an API key in User Settings > API Keys
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Service Account">
|
||||||
|
The Service Account mode uses an Access Key to authenticate with the API and a Public Key and Private Key to perform any cryptographic operations.
|
||||||
|
|
||||||
|
To authenticate requests with Infisical using the Access Key, you must include it in the `Authorization` header of HTTP requests made to the platform with the value `Bearer <access_key>`.
|
||||||
|
|
||||||
|
You can create a Service Account in Organization Settings > Service Accounts
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="Infisical Token">
|
<Accordion title="Infisical Token">
|
||||||
To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer st.<rest_of_your_infisical_token>`.
|
|
||||||
|
The Infisical Token mode uses an Infisical Token to authenticate with the API.
|
||||||
|
|
||||||
|
To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer <infisical_token>`.
|
||||||
|
|
||||||
You can obtain an Infisical Token in Project Settings > Service Tokens.
|
You can obtain an Infisical Token in Project Settings > Service Tokens.
|
||||||
|
|
||||||

|

|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
Depending on your use case, it may make sense to use one or another authentication mode:
|
||||||
|
|
||||||
|
- API Key (not recommended): Use if you need full access to the Public API without needing to access any secrets endpoints (because API keys can't encrypt/decrypt secrets).
|
||||||
|
- Service Account (recommeded): Use if you need access to multiple projects and environments in an organization; service accounts can generate short-lived access tokens, making them useful for some complex setups.
|
||||||
|
- Service Token (recommeded): Use if you need short-lived, scoped CRUD access to the secrets of a specific project and environment.
|
||||||
@@ -2,11 +2,17 @@
|
|||||||
title: "Introduction"
|
title: "Introduction"
|
||||||
---
|
---
|
||||||
|
|
||||||
Infisical's REST API provides users an alternative way to programmatically access and manage
|
Infisical's Public (REST) API provides users an alternative way to programmatically access and manage
|
||||||
secrets via HTTPS requests. This can be useful for automating tasks, such as
|
secrets via HTTPS requests. This can be useful for automating tasks, such as
|
||||||
rotating credentials, or for integrating secret management into a larger system.
|
rotating credentials, or for integrating secret management into a larger system.
|
||||||
|
|
||||||
With the REST API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
With the Public API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We highly recommend using one of the available SDKs when working with the Infisical API.
|
||||||
|
|
||||||
|
If you decide to make your own requests using the API reference instead, be prepared for a steeper learning curve and more manual work.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ To add a member to your organization, scroll down to the "Organization Members"
|
|||||||
projects by default.
|
projects by default.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
## Service Accounts
|
||||||
|
|
||||||
|
Service accounts represent machine identities such as VMs or application clients that can authenticate with Infisical. They can be provisioned read/write permissions for project(s) and environment(s).
|
||||||
|
|
||||||
|
To add a service account to your organization, scroll down to the "Service Accounts" section and create a service account. Afterwards, you can press on the edit button beside the service account to provision it permissions.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Incident contacts
|
## Incident contacts
|
||||||
|
|
||||||
Incident contacts of an organization are alerted if anything abnormal is detected within the operations of an organization.
|
Incident contacts of an organization are alerted if anything abnormal is detected within the operations of an organization.
|
||||||
|
|||||||
@@ -25,16 +25,16 @@ In most cases, environment variables belong to specific environments: developmen
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Personal/Shared scoping
|
### Personal overrides
|
||||||
|
|
||||||
Every environment variable is classified as either personal or shared.
|
Every environment variable value can be overriden with a custom value.
|
||||||
|
|
||||||
- A personal environment variable is one created by a user of a project to be available for that user only.
|
- An overriden value can only be read and accesssed by the user that overrode the original shared value.
|
||||||
- A shared environment variable is one created by a user of a project to be available for other users of the project.
|
- A (default) shared value can be read and accesssed by other users in a project.
|
||||||
|
|
||||||
You can toggle the classification of an environment variable by pressing on its settings:
|
You can turn overrides on/off by toggling the override/branch icon:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Search
|
### Search
|
||||||
|
|
||||||
@@ -42,12 +42,6 @@ You can search for any environment variable by its key.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Sort
|
|
||||||
|
|
||||||
You can sort environment variables alphabetically by their keys.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Hide/Un-hide
|
### Hide/Un-hide
|
||||||
|
|
||||||
You can hide or un-hide the values of your environment variables. By default, the values are hidden for your privacy.
|
You can hide or un-hide the values of your environment variables. By default, the values are hidden for your privacy.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: "Introduction"
|
|||||||
description: "What is Infisical?"
|
description: "What is Infisical?"
|
||||||
---
|
---
|
||||||
|
|
||||||
Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret manager that enables teams to easily manage and sync their environment variables.
|
Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret management platform that enables teams to easily manage and sync their environment variables.
|
||||||
|
|
||||||
Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
|
Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Quickstart"
|
title: "Quickstart"
|
||||||
description: "Start managing your developer secrets and configs with Infisical in 10 minutes."
|
description: "Start managing developer secrets and configs with Infisical in minutes."
|
||||||
---
|
---
|
||||||
|
|
||||||
These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application.
|
These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application.
|
||||||
@@ -9,7 +9,7 @@ These examples demonstrate how to store and fetch environment variables from [In
|
|||||||
|
|
||||||
1. Login or create an account at `app.infisical.com`.
|
1. Login or create an account at `app.infisical.com`.
|
||||||
2. Create a new project.
|
2. Create a new project.
|
||||||
3. Populate your environment variables as in the image below.
|
3. Keep the default environment variables or populate them as in the image below.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 744 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 717 KiB |
|
After Width: | Height: | Size: 504 KiB |
|
After Width: | Height: | Size: 538 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 622 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 636 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 727 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 680 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 1024 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 686 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 718 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 1.0 MiB |
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "CircleCI"
|
title: "CircleCI"
|
||||||
description: "How to automatically sync secrets from Infisical into your CircleCI project."
|
description: "How to sync secrets from Infisical to CircleCI"
|
||||||
---
|
---
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "GitHub Actions"
|
title: "GitHub Actions"
|
||||||
description: "How to automatically sync secrets from Infisical into your GitHub Actions."
|
description: "How to sync secrets from Infisical to GitHub Actions"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "GitLab"
|
title: "GitLab"
|
||||||
description: "How to automatically sync secrets from Infisical into GitLab."
|
description: "How to sync secrets from Infisical to GitLab"
|
||||||
---
|
---
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Travis CI"
|
title: "Travis CI"
|
||||||
description: "How to automatically sync secrets from Infisical to your Travis CI repository."
|
description: "How to sync secrets from Infisical to Travis CI"
|
||||||
---
|
---
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "AWS Parameter Store"
|
title: "AWS Parameter Store"
|
||||||
description: "How to automatically sync secrets from Infisical to your AWS Parameter Store."
|
description: "How to sync secrets from Infisical to AWS Parameter Store"
|
||||||
---
|
---
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "AWS Secret Manager"
|
title: "AWS Secret Manager"
|
||||||
description: "How to automatically sync secrets from Infisical to your AWS Secret Manager."
|
description: "How to sync secrets from Infisical to AWS Secret Manager"
|
||||||
---
|
---
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Azure Key Vault"
|
title: "Azure Key Vault"
|
||||||
description: "How to automatically sync secrets from Infisical into your Azure Key Vault."
|
description: "How to sync secrets from Infisical to Azure Key Vault"
|
||||||
---
|
---
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||