Merge remote-tracking branch 'origin' into secrets-v3

This commit is contained in:
Tuan Dang
2023-04-14 17:48:23 +03:00
127 changed files with 3797 additions and 516 deletions

View File

@ -9,6 +9,12 @@ jobs:
steps:
- name: ☁️ Checkout source
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
id: commit
uses: pr-mpt/actions-commit-hash@v2
@ -45,8 +51,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest
tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
@ -94,8 +100,8 @@ jobs:
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest
tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@ -122,7 +128,7 @@ jobs:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- 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
- name: switch to gamma namespace
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
run: kubectl get ingress
@ -135,4 +141,4 @@ jobs:
exit 1
else
echo "Helm upgrade was successful"
fi
fi

View File

@ -62,7 +62,7 @@
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.2.4",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",
@ -3629,9 +3629,9 @@
}
},
"node_modules/@types/jest": {
"version": "29.4.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz",
"integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==",
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@ -15642,9 +15642,9 @@
}
},
"@types/jest": {
"version": "29.4.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz",
"integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==",
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
"dev": true,
"requires": {
"expect": "^29.0.0",

View File

@ -3,8 +3,8 @@
"@aws-sdk/client-secrets-manager": "^3.287.0",
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/tracing": "^7.39.0",
"@sentry/node": "^7.40.0",
"@sentry/tracing": "^7.39.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
@ -57,7 +57,7 @@
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles",
"test": "cross-env NODE_ENV=test jest --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",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
@ -80,7 +80,7 @@
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.2.4",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",

View File

@ -13,7 +13,7 @@ export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
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 getLokiHost = () => infisical.get('LOKI_HOST')!;
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 getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
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
}

View File

@ -15,10 +15,10 @@ import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtRefreshSecret,
getJwtAuthLifetime,
getJwtAuthSecret
getJwtAuthSecret,
getHttpsEnabled
} from '../../config';
declare module 'jsonwebtoken' {
@ -126,21 +126,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
@ -182,14 +182,14 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled() as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],

View File

@ -17,9 +17,9 @@ import {
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtMfaLifetime,
getJwtMfaSecret
getJwtMfaSecret,
getHttpsEnabled
} from '../../config';
declare module 'jsonwebtoken' {
@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
// case: user does not have MFA enablgged
@ -302,7 +302,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
interface VerifyMfaTokenRes {

View File

@ -550,7 +550,10 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId 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 = [];
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
if (tagNamesList != undefined && tagNamesList.length != 0) {
@ -560,72 +563,71 @@ export const getSecrets = async (req: Request, res: Response) => {
return tag ? tag.id : null;
});
}
let secrets: ISecret[] = [];
if (req.user) {
// case: client authorization is via JWT
let hasWriteOnlyAccess
if (!req.serviceTokenData) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
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" })
}
const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
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;
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
secretQuery = {
workspace: workspaceId,
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] }
}
const secretQuery: any = {
workspace: workspaceId,
environment,
$or: [
{ user: req.user._id }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
if (hasWriteOnlyAccess) {
// (i.e. you don't get values to decrypt since you can only write)
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
// only return the secret keys and not the values since user does not have right to see values
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
}
}
if (req.serviceAccount || req.serviceTokenData) {
// case: client authorization is either via service account or service token
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
// case: client authorization is via service token
if (req.serviceTokenData) {
const userId = req.serviceTokenData.user._id
const secretQuery: any = {
workspace: workspaceId,
environment,
user: {
$exists: false
},
...(tagIds.length > 0 ? { tags: { $in: tagIds } } : {}),
type: SECRET_SHARED
});
$or: [
{ user: userId }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
// TODO check if service token has write only permission
secrets = await Secret.find(secretQuery).populate("tags");
}
// case: client authorization is via service account
if (req.serviceAccount) {
const secretQuery: any = {
workspace: workspaceId,
environment,
user: { $exists: false } // shared secrets only from workspace
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
secrets = await Secret.find(secretQuery).populate("tags");
}
const channel = getChannelFromUserAgent(req.headers['user-agent'])
@ -638,7 +640,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
@ -952,7 +954,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/
return res.status(200).send({
message: 'delete secrets!!'
});

View File

@ -8,7 +8,7 @@ import {
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
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
@ -24,9 +24,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -38,9 +38,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
@ -48,11 +48,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
salt: string;
verifier: string;
organizationName: string;
} = req.body;
} = req.body;
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
@ -66,10 +66,10 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
@ -158,9 +158,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -192,10 +192,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
@ -241,7 +241,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
user,

View File

@ -15,32 +15,28 @@ import {
const requireSecretSnapshotAuth = ({
acceptedRoles,
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
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' }));
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id,
workspaceId: secretSnapshot.workspace,
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
}
}

View File

@ -7,7 +7,12 @@ import {
} from '../../../middleware';
import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../../variables';
import {
ADMIN,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../../variables';
router.get(
'/:secretId/secret-versions',
@ -15,7 +20,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
param('secretId').exists().trim(),
query('offset').exists().isInt(),
@ -30,7 +36,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}),
param('secretId').exists().trim(),
body('version').exists().isInt(),

View File

@ -148,7 +148,7 @@ const getAuthSTDPayload = async ({
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
@ -157,8 +157,8 @@ const getAuthSTDPayload = async ({
}, {
new: true
})
.select('+encryptedKey +iv +tag');
.select('+encryptedKey +iv +tag').populate('user serviceAccount');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
return serviceTokenData;
@ -176,20 +176,20 @@ const getAuthSAAKPayload = async ({
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const serviceAccount = await ServiceAccount.findById(
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
).select('+secretHash');
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
if (!result) throw UnauthorizedRequestError({
message: 'Failed to authenticate service account access key'
});
return serviceAccount;
}
@ -208,7 +208,7 @@ const getAuthAPIKeyPayload = async ({
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate<{user: IUser}>('user', '+publicKey');
.populate<{ user: IUser }>('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
@ -232,13 +232,13 @@ const getAuthAPIKeyPayload = async ({
}, {
new: true
});
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
}
const user = await User.findById(apiKeyData.user).select('+publicKey');
if (!user) {
throw AccountNotFoundError({
message: 'Failed to find user'

View File

@ -1,10 +1,16 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
BotKey,
Secret,
ISecret,
IUser
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
generateKeyPair,
@ -12,8 +18,88 @@ import {
decryptSymmetric,
decryptAsymmetric
} 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 { 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]
@ -222,6 +308,7 @@ const decryptSymmetricHelper = async ({
}
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,

View File

@ -1,17 +1,42 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
Integration,
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import {
UnauthorizedRequestError,
IntegrationAuthNotFoundError,
IntegrationNotFoundError
} from '../utils/errors';
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 {
workspace: string;
@ -20,6 +45,84 @@ interface Update {
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
* named [integration]
@ -140,7 +243,7 @@ const syncIntegrationsHelper = async ({
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString()
integrationAuthId: integration.integrationAuth
});
// sync secrets to integration
@ -167,7 +270,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken;
try {
@ -204,7 +307,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
try {
@ -367,6 +470,7 @@ const setIntegrationAuthAccessHelper = async ({
}
export {
validateClientForIntegration,
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,

View File

@ -0,0 +1,108 @@
import { Types } from 'mongoose';
import {
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IWorkspace
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
IntegrationAuthNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
/**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationAuthId - id of integration authorization to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegrationAuth = async ({
authData,
integrationAuthId,
acceptedRoles,
attachAccessToken
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationAuthId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean;
}) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
let accessToken;
if (attachAccessToken) {
accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integrationAuth.workspace._id
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration authorization'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration authorization'
});
}
export {
validateClientForIntegrationAuth
};

View File

@ -1,10 +1,106 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Membership, Key } from '../models';
import {
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipNotFoundError,
BadRequestError
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
/**
* Validate authenticated clients for membership with id [membershipId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
* @returns {Membership} - validated membership
*/
const validateClientForMembership = async ({
authData,
membershipId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const membership = await Membership.findById(membershipId);
if (!membership) throw MembershipNotFoundError({
message: 'Failed to find membership'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: membership.workspace
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: new Types.ObjectId(membership.workspace)
});
return membership;
}
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for membership'
});
}
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@ -21,7 +117,7 @@ const validateMembership = async ({
}: {
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
acceptedRoles?: string[];
acceptedRoles?: Array<'admin' | 'member'>;
}) => {
const membership = await Membership.findOne({
@ -35,7 +131,7 @@ const validateMembership = async ({
if (acceptedRoles) {
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 {
validateClientForMembership,
validateMembership,
addMemberships,
findMembership,

View File

@ -1,10 +1,98 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { MembershipOrg, Workspace, Membership, Key } from '../models';
import {
MembershipOrg,
Workspace,
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipOrgNotFoundError,
BadRequestError
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate authenticated clients for organization membership with id [membershipOrgId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
* @param {MembershipOrg} - validated organization membership
*/
const validateClientForMembershipOrg = async ({
authData,
membershipOrgId,
acceptedRoles,
acceptedStatuses
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipOrgId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
if (!membershipOrg) throw MembershipOrgNotFoundError({
message: 'Failed to find organization membership '
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization membership'
});
}
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
@ -22,8 +110,8 @@ const validateMembershipOrg = async ({
}: {
userId: Types.ObjectId;
organizationId: Types.ObjectId;
acceptedRoles: string[];
acceptedStatuses: string[];
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses?: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await MembershipOrg.findOne({
user: userId,
@ -34,12 +122,16 @@ const validateMembershipOrg = async ({
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
}
if (!acceptedRoles.includes(membershipOrg.role)) {
throw BadRequestError({ message: 'Failed to validate organization membership role' });
if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
}
}
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw BadRequestError({ message: 'Failed to validate organization membership status' });
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
}
}
return membershipOrg;
@ -164,6 +256,7 @@ const deleteMembershipOrg = async ({
};
export {
validateClientForMembershipOrg,
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,

View File

@ -15,7 +15,8 @@ import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
AUTH_MODE_API_KEY,
OWNER
} from '../variables';
import {
getStripeSecretKey,
@ -24,8 +25,15 @@ import {
getStripeProductStarter
} from '../config';
import {
UnauthorizedRequestError
UnauthorizedRequestError,
OrganizationNotFoundError
} from '../utils/errors';
import {
validateUserClientForOrganization
} from '../helpers/user';
import {
validateServiceAccountClientForOrganization
} from '../helpers/serviceAccount';
/**
* Validate accepted clients for organization with id [organizationId]
@ -35,34 +43,66 @@ import {
*/
const validateClientForOrganization = async ({
authData,
organizationId
organizationId,
acceptedRoles,
acceptedStatuses
}: {
authData: {
authMode: string;
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) {
// 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) {
// TODO
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization
});
return ({ organization });
}
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) {
// TODO
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses
});
return ({ organization, membershipOrg });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization resource'
message: 'Failed client authorization for organization'
});
}
@ -228,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity

View File

@ -15,7 +15,7 @@ const apiLimiter = rateLimit({
});
// 10 requests per minute
const authLimiter = rateLimit({
const authLimit = rateLimit({
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
@ -36,8 +36,16 @@ const passwordLimiter = rateLimit({
}
});
export {
apiLimiter,
authLimiter,
passwordLimiter
const authLimiter = (req: any, res: any, next: any) => {
if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
authLimiter,
passwordLimiter
};

View File

@ -10,15 +10,24 @@ import {
ISecret
} from '../models';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForSecret,
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets
validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets
validateServiceAccountClientForSecrets,
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
@ -27,12 +36,91 @@ import {
} 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 {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {ServiceTokenData} obj.service - service token client
* @param {String[]} obj.secretIds - ids of secrets to validate against
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecret = async ({
authData,
secretId,
acceptedRoles,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
const secret = await Secret.findById(secretId);
if (!secret) throw SecretNotFoundError({
message: 'Failed to find secret'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment
});
return secret;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secret'
});
}
/**
* Validate authenticated clients for secrets with ids [secretIds] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecrets = async ({
authData,
@ -43,7 +131,7 @@ const validateClientForSecrets = async ({
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretIds: string[];
secretIds: Types.ObjectId[];
requiredPermissions: string[];
}) => {
@ -51,7 +139,7 @@ const validateClientForSecrets = async ({
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
$in: secretIds
}
});
@ -105,5 +193,6 @@ const validateClientForSecrets = async ({
}
export {
validateClientForSecret,
validateClientForSecrets
}

View File

@ -8,6 +8,8 @@ import {
ServiceTokenData,
IServiceTokenData,
ISecret,
IOrganization,
IServiceAccountWorkspacePermission,
ServiceAccountWorkspacePermission
} from '../models';
import {
@ -109,21 +111,20 @@ const validateClientForServiceAccount = async ({
environment?: string;
requiredPermissions?: string[];
}) => {
// TODO: add service account API support for workspace-level endpoints that are not
// tied to any specific 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({
serviceAccount,
workspace: new Types.ObjectId(workspaceId),
environment
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
// TODO: refactor
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
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) => {
// TODO: refactor
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
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 {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount
validateServiceAccountClientForServiceAccount,
validateServiceAccountClientForOrganization
}

View File

@ -1,9 +1,94 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData
IServiceTokenData,
ServiceTokenData,
IUser,
User,
IServiceAccount,
ServiceAccount,
} 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
@ -34,20 +119,24 @@ import { UnauthorizedRequestError } from '../utils/errors';
});
}
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
if (environment) {
// case: environment is specified
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment 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 {
validateClientForServiceTokenData,
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}

View File

@ -5,7 +5,9 @@ import {
ISecret,
IServiceAccount,
User,
Membership
Membership,
IOrganization,
Organization,
} from '../models';
import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
@ -177,21 +179,23 @@ const validateUserClientForWorkspace = async ({
user,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
// validate user membership in workspace
const membership = await validateMembership({
userId: user._id,
workspaceId
workspaceId,
acceptedRoles
});
// TODO: refactor
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
@ -215,6 +219,42 @@ const validateUserClientForWorkspace = async ({
return membership;
}
/**
* Validate that user (client) can access secret [secret]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecret = async ({
user,
secret,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
secret: ISecret;
acceptedRoles?: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const membership = await validateMembership({
userId: user._id,
workspaceId: secret.workspace,
acceptedRoles
});
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
}
/**
* Validate that user (client) can access secrets [secrets]
* with required permissions [requiredPermissions]
@ -232,7 +272,8 @@ const validateUserClientForWorkspace = async ({
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
// TODO: refactor
// TODO: add acceptedRoles?
const userMemberships = await Membership.find({ user: user._id })
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 {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets,
validateUserClientForServiceAccount
validateUserClientForServiceAccount,
validateUserClientForOrganization,
validateUserClientForSecret
};

View File

@ -19,7 +19,7 @@ import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
@ -34,28 +34,38 @@ import {
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
@ -89,6 +99,7 @@ const validateClientForWorkspace = async ({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
@ -96,7 +107,7 @@ const validateClientForWorkspace = async ({
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace resource'
message: 'Failed client authorization for workspace'
});
}

View File

@ -48,18 +48,18 @@ import {
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
@ -172,7 +172,7 @@ const main = async () => {
getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
});
createTestUserForDevelopment();
await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {

View File

@ -16,6 +16,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
@ -25,6 +26,7 @@ import {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables";
interface App {
@ -116,6 +118,11 @@ const getApps = async ({
accessToken,
})
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
@ -608,4 +615,40 @@ const getAppsGitlab = async ({
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 };

View File

@ -25,6 +25,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
@ -34,6 +35,7 @@ import {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables";
import request from '../config/request';
import axios from "axios";
@ -157,6 +159,13 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken
});
break;
}
} catch (err) {
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 };

View File

@ -44,6 +44,7 @@ const requireAuth = ({
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({
@ -87,7 +88,7 @@ const requireAuth = ({
req.authData = {
authMode,
authPayload
authPayload // User, ServiceAccount, ServiceTokenData
}
return next();

View File

@ -1,32 +1,28 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { validateClientForBot } from '../helpers/bot';
import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
const requireBotAuth = ({
acceptedRoles,
location = 'params'
locationBotId = 'params'
}: {
acceptedRoles: string[];
location?: req;
acceptedRoles: Array<'admin' | 'member'>;
locationBotId?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const bot = await Bot.findById(req[location].botId);
const { botId } = req[locationBotId];
if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
}
await validateMembership({
userId: req.user._id,
workspaceId: bot.workspace,
req.bot = await validateClientForBot({
authData: req.authData,
botId: new Types.ObjectId(botId),
acceptedRoles
});
req.bot = bot;
next();
}
}

View File

@ -1,7 +1,9 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { validateClientForIntegration } from '../helpers/integration';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/**
@ -13,42 +15,24 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err
const requireIntegrationAuth = ({
acceptedRoles
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware
const { integrationId } = req.params;
// validate integration accessibility
const integration = await Integration.findOne({
_id: integrationId
});
if (!integration) {
return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
}
await validateMembership({
userId: req.user._id,
workspaceId: integration.workspace,
const { integration, accessToken } = await validateClientForIntegration({
authData: req.authData,
integrationId: new Types.ObjectId(integrationId),
acceptedRoles
});
const integrationAuth = await IntegrationAuth.findOne({
_id: integration.integrationAuth
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}))
if (integration) {
req.integration = integration;
}
if (accessToken) {
req.accessToken = accessToken;
}
req.integration = integration;
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
return next();
};

View File

@ -1,7 +1,9 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models';
import { IntegrationService } from '../services';
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
@ -19,36 +21,26 @@ const requireIntegrationAuthorizationAuth = ({
attachAccessToken = true,
location = 'params'
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
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) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'}))
}
await validateMembership({
userId: req.user._id,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({
authData: req.authData,
integrationAuthId: new Types.ObjectId(integrationAuthId),
acceptedRoles,
attachAccessToken
});
if (integrationAuth) {
req.integrationAuth = integrationAuth;
}
req.integrationAuth = integrationAuth;
if (attachAccessToken) {
const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
req.accessToken = access.accessToken;
if (accessToken) {
req.accessToken = accessToken;
}
return next();

View File

@ -1,9 +1,13 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
Membership,
} from '../models';
import { validateMembership } from '../helpers/membership';
import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
type req = 'params' | 'body' | 'query';
@ -16,43 +20,25 @@ type req = 'params' | 'body' | 'query';
*/
const requireMembershipAuth = ({
acceptedRoles,
location = 'params'
locationMembershipId = 'params'
}: {
acceptedRoles: string[];
location?: req;
acceptedRoles: Array<'admin' | 'member'>;
locationMembershipId: req
}) => {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const { membershipId } = req[location];
const membership = await Membership.findById(membershipId);
if (!membership) throw new Error('Failed to find target membership');
const userMembership = await Membership.findOne({
workspace: membership.workspace
});
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'
}));
}
const { membershipId } = req[locationMembershipId];
req.targetMembership = await validateClientForMembership({
authData: req.authData,
membershipId: new Types.ObjectId(membershipId),
acceptedRoles
});
return next();
}
}

View File

@ -1,11 +1,17 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import { validateMembershipOrg } from '../helpers/membershipOrg';
import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
// TODO: transform
type req = 'params' | 'body' | 'query';
/**
@ -18,32 +24,23 @@ type req = 'params' | 'body' | 'query';
const requireMembershipOrgAuth = ({
acceptedRoles,
acceptedStatuses,
location = 'params'
locationMembershipOrgId = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
location?: req;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
locationMembershipOrgId?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { membershipId } = req[location];
const membershipOrg = await MembershipOrg.findById(membershipId);
if (!membershipOrg) throw new Error('Failed to find target organization membership');
req.targetMembership = await validateMembershipOrg({
userId: req.user._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate organization membership'
}));
}
const { membershipId } = req[locationMembershipOrgId];
req.membershipOrg = await validateClientForMembershipOrg({
authData: req.authData,
membershipOrgId: new Types.ObjectId(membershipId),
acceptedRoles,
acceptedStatuses
});
return next();
}
}

View File

@ -3,6 +3,7 @@ import { Types } from 'mongoose';
import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { validateMembershipOrg } from '../helpers/membershipOrg';
import { validateClientForOrganization } from '../helpers/organization';
type req = 'params' | 'body' | 'query';
@ -16,20 +17,29 @@ type req = 'params' | 'body' | 'query';
const requireOrganizationAuth = ({
acceptedRoles,
acceptedStatuses,
location = 'params'
locationOrganizationId = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
location?: req;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
locationOrganizationId?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { organizationId } = req[location];
req.membershipOrg = await validateMembershipOrg({
userId: req.user._id,
const { organizationId } = req[locationOrganizationId];
const { organization, membershipOrg } = await validateClientForOrganization({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId),
acceptedRoles,
acceptedStatuses
});
if (organization) {
req.organization = organization;
}
if (membershipOrg) {
req.membershipOrg = membershipOrg;
}
return next();
};

View File

@ -1,12 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
import { Secret } from '../models';
import {
validateMembership
} from '../helpers/membership';
import {
validateClientForSecret
} from '../helpers/secrets';
// 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.
@ -15,34 +20,25 @@ import {
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireSecretAuth = ({
acceptedRoles
acceptedRoles,
requiredPermissions
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretId } = req.params;
const secret = await Secret.findById(secretId);
if (!secret) {
return next(SecretNotFoundError({
message: 'Failed to find secret'
}));
}
await validateMembership({
userId: req.user._id,
workspaceId: secret.workspace,
acceptedRoles
});
req._secret = secret;
const { secretId } = req.params;
const secret = await validateClientForSecret({
authData: req.authData,
secretId: new Types.ObjectId(secretId),
acceptedRoles,
requiredPermissions
});
req._secret = secret;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' }));
}
next();
}
}

View File

@ -1,4 +1,5 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models';
import { validateClientForSecrets } from '../helpers/secrets';
@ -24,7 +25,7 @@ const requireSecretsAuth = ({
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: [req.body.secretIds],
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions
});

View File

@ -14,8 +14,8 @@ const requireServiceAccountWorkspacePermissionAuth = ({
acceptedStatuses,
location = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {

View File

@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceToken, ServiceTokenData } from '../models';
import { validateClientForServiceTokenData } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
@ -9,30 +11,17 @@ const requireServiceTokenDataAuth = ({
acceptedRoles,
location = 'params'
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { serviceTokenDataId } = req[location];
const serviceTokenData = await ServiceTokenData
.findById(req[location].serviceTokenDataId)
.select('+encryptedKey +iv +tag').populate('user');
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;
req.serviceTokenData = await validateClientForServiceTokenData({
authData: req.authData,
serviceTokenDataId: new Types.ObjectId(serviceTokenDataId),
acceptedRoles
});
next();
}

View File

@ -19,13 +19,12 @@ const requireWorkspaceAuth = ({
locationEnvironment = undefined,
requiredPermissions = []
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
@ -34,6 +33,7 @@ const requireWorkspaceAuth = ({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
acceptedRoles,
requiredPermissions
});

View File

@ -13,6 +13,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
} from "../variables";
export interface IIntegration {
@ -42,7 +43,8 @@ export interface IIntegration {
| 'railway'
| 'flyio'
| 'circleci'
| 'travisci';
| 'travisci'
| 'supabase';
integrationAuth: Types.ObjectId;
}
@ -122,6 +124,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
],
required: true,
},

View File

@ -1,4 +1,4 @@
import { Schema, model, Types } from "mongoose";
import { Schema, model, Types, Document } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -13,12 +13,13 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
} from "../variables";
export interface IIntegrationAuth {
export interface IIntegrationAuth extends Document {
_id: 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;
accountId: string;
refreshCiphertext?: string;
@ -56,6 +57,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
],
required: true,
},

View File

@ -1,7 +1,7 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types, Document } from 'mongoose';
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables';
export interface IMembershipOrg {
export interface IMembershipOrg extends Document {
_id: Types.ObjectId;
user: Types.ObjectId;
inviteEmail: string;

View File

@ -10,7 +10,9 @@ import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN
AUTH_MODE_SERVICE_TOKEN,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2';
@ -75,7 +77,8 @@ router.get(
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
validateRequest,
secretController.getSecret
@ -103,7 +106,8 @@ router.delete(
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}),
param('secretId').isMongoId(),
validateRequest,

View File

@ -1,5 +1,6 @@
import express from 'express';
const router = express.Router();
import { Types } from 'mongoose';
import {
requireAuth,
requireWorkspaceAuth,
@ -47,7 +48,7 @@ router.post(
if (secretIds.length > 0) {
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds,
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions: []
});
}

View File

@ -53,7 +53,7 @@ router.post(
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
location: 'body'
locationOrganizationId: 'body'
}),
serviceAccountsController.createServiceAccount
);

View File

@ -106,7 +106,8 @@ router.patch( // TODO - rewire dashboard to this route
locationWorkspaceId: 'params'
}),
requireMembershipAuth({
acceptedRoles: [ADMIN]
acceptedRoles: [ADMIN],
locationMembershipId: 'params'
}),
workspaceController.updateWorkspaceMembership
);
@ -124,7 +125,8 @@ router.delete( // TODO - rewire dashboard to this route
locationWorkspaceId: 'params'
}),
requireMembershipAuth({
acceptedRoles: [ADMIN]
acceptedRoles: [ADMIN],
locationMembershipId: 'params'
}),
workspaceController.deleteWorkspaceMembership
);

View File

@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
@ -67,7 +68,7 @@ class IntegrationService {
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: Types.ObjectId}) {
return await getIntegrationAuthRefreshHelper({
integrationAuthId
});
@ -80,7 +81,7 @@ class IntegrationService {
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} accessToken - decrypted access token
*/
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) {
return await getIntegrationAuthAccessHelper({
integrationAuthId
});

View File

@ -8,7 +8,9 @@ import {
} from '../config';
import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData
} from '../models';
import {
@ -56,7 +58,7 @@ class Telemetry {
}: {
user?: IUser;
serviceAccount?: IServiceAccount;
serviceTokenData?: IServiceTokenData;
serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated)
}) => {
let distinctId = '';
@ -65,11 +67,13 @@ class Telemetry {
}
if (serviceAccount) {
distinctId = `sa.${serviceAccount._id}`;
distinctId = `sa.${serviceAccount._id.toString()}`;
}
if (serviceTokenData) {
distinctId = `st.${serviceTokenData._id}`;
if (serviceTokenData?.user && serviceTokenData?.user instanceof User) {
distinctId = serviceTokenData.user.email;
} else if (serviceTokenData?.serviceAccount && serviceTokenData?.serviceAccount instanceof ServiceAccount) {
distinctId = `sa.${serviceTokenData.serviceAccount._id.toString()}`;
}
if (distinctId === '') {

View File

@ -8,17 +8,18 @@ import { Key, Membership, MembershipOrg, Organization, User, Workspace } from ".
import { Types } from 'mongoose';
import { getNodeEnv } from '../config';
export const createTestUserForDevelopment = async () => {
if (getNodeEnv() === "development") {
const testUserEmail = "test@localhost.local"
const testUserPassword = "testInfisical1"
const testUserId = "63cefa6ec8d3175601cfa980"
const testWorkspaceId = "63cefb15c8d3175601cfa989"
const testOrgId = "63cefb15c8d3175601cfa985"
const testMembershipId = "63cefb159185d9aa3ef0cf35"
const testMembershipOrgId = "63cefb159185d9aa3ef0cf31"
const testWorkspaceKeyId = "63cf48f0225e6955acec5eff"
export const testUserEmail = "test@localhost.local"
export const testUserPassword = "testInfisical1"
export const testUserId = "63cefa6ec8d3175601cfa980"
export const testWorkspaceId = "63cefb15c8d3175601cfa989"
export const testOrgId = "63cefb15c8d3175601cfa985"
export const testMembershipId = "63cefb159185d9aa3ef0cf35"
export const testMembershipOrgId = "63cefb159185d9aa3ef0cf31"
export const testWorkspaceKeyId = "63cf48f0225e6955acec5eff"
export const plainTextWorkspaceKey = "543fef8224813a46230b0a50a46c5fb2"
export const createTestUserForDevelopment = async () => {
if (getNodeEnv() === "development" || getNodeEnv() === "test") {
const testUser = {
_id: testUserId,
email: testUserEmail,

View File

@ -73,6 +73,16 @@ export const ValidationError = (error?: Partial<RequestErrorContext>) => new Req
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]<-----
export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
@ -202,4 +212,13 @@ export const ServiceAccountKeyNotFoundError = (error?: Partial<RequestErrorConte
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]<-----

View File

@ -19,6 +19,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@ -36,6 +37,7 @@ import {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
getIntegrationOptions
} from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
@ -102,6 +104,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@ -119,6 +122,7 @@ export {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_LOGIN,

View File

@ -21,6 +21,7 @@ const INTEGRATION_RAILWAY = "railway";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_TRAVISCI = "travisci";
const INTEGRATION_SUPABASE = 'supabase';
const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@ -32,6 +33,7 @@ const INTEGRATION_SET = new Set([
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
]);
// 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_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
const getIntegrationOptions = () => {
const INTEGRATION_OPTIONS = [
@ -178,6 +181,15 @@ const getIntegrationOptions = () => {
clientId: '',
docsLink: ''
},
{
name: 'Supabase',
slug: 'supabase',
image: 'Supabase.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
@ -207,6 +219,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@ -224,5 +237,6 @@ export {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
getIntegrationOptions
};

View File

@ -3,3 +3,8 @@ process.env.MONGO_URL =
'mongodb://test:test1234@localhost:27018/?authSource=admin';
process.env.MONGO_USERNAME = 'test';
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"

View File

@ -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=="
}
}
]

View File

@ -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=="
}
}
]

View File

@ -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=="
}
}
]

View File

@ -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()
}

View File

@ -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)
})
})
})

View File

@ -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: 'test@localhost.local',
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),
});
})
})
})

View File

@ -4,7 +4,7 @@ import {
decryptSymmetric,
encryptAsymmetric,
encryptSymmetric
} from '../../src/utils/crypto';
} from '../../../src/utils/crypto';
describe('Crypto', () => {
describe('encryptAsymmetric', () => {

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from '@jest/globals';
import { getChannelFromUserAgent } from '../../src/utils/posthog';
import { getChannelFromUserAgent } from '../../../src/utils/posthog';
describe('posthog getChannelFromUserAgent', () => {
test("should return 'web' when userAgent includes 'mozilla'", () => {

View File

@ -230,19 +230,10 @@ type GetEncryptedSecretsV2Response struct {
}
type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
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"`
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
ExpiresAt time.Time `json:"expiresAt"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`

View File

@ -96,6 +96,7 @@ Resources:
echo "JWT_AUTH_SECRET=${!JWT_AUTH_SECRET}" >> .env
echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env
echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env
echo "HTTPS_ENABLED=false" >> .env
docker-compose up -d

View File

@ -1,25 +1,51 @@
---
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.
- 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.
## Essentials
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>
<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.
You can obtain an API key in User Settings > API Keys
![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
</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 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.
![token add](../../images/project-token-add.png)
</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.

View File

@ -2,11 +2,17 @@
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
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

View File

@ -24,6 +24,14 @@ To add a member to your organization, scroll down to the "Organization Members"
projects by default.
</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.
![organization service accounts](../../images/organization-service-accounts.png)
## Incident contacts
Incident contacts of an organization are alerted if anything abnormal is detected within the operations of an organization.

View File

@ -25,16 +25,16 @@ In most cases, environment variables belong to specific environments: developmen
![project environment](../../images/project-environment.png)
### 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.
- A shared environment variable is one created by a user of a project to be available for other users of the project.
- An overriden value can only be read and accesssed by the user that overrode the original shared value.
- 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:
![project variable toggle open](../../images/project-envar-toggle-open.png)
![project variable toggle open](../../images/project-envar-override.png)
### Search
@ -42,12 +42,6 @@ You can search for any environment variable by its key.
![project search](../../images/project-search.png)
### Sort
You can sort environment variables alphabetically by their keys.
![project sort](../../images/project-sort.png)
### Hide/Un-hide
You can hide or un-hide the values of your environment variables. By default, the values are hidden for your privacy.

View File

@ -3,7 +3,7 @@ title: "Introduction"
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.

View File

@ -1,6 +1,6 @@
---
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.
@ -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`.
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.
![project quickstart](../images/project-quickstart.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 717 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -1,6 +1,6 @@
---
title: "CircleCI"
description: "How to automatically sync secrets from Infisical into your CircleCI project."
description: "How to sync secrets from Infisical to CircleCI"
---
Prerequisites:

View File

@ -1,6 +1,6 @@
---
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>

View File

@ -1,6 +1,6 @@
---
title: "GitLab"
description: "How to automatically sync secrets from Infisical into GitLab."
description: "How to sync secrets from Infisical to GitLab"
---
Prerequisites:

View File

@ -1,6 +1,6 @@
---
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:

View File

@ -1,6 +1,6 @@
---
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:

View File

@ -1,6 +1,6 @@
---
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:

View File

@ -1,6 +1,6 @@
---
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:

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